#!/usr/bin/python # -*- coding: utf-8 -*- ########################################################################### ## ## ## Copyright (C) 2009 Stéphane Péchard <stephanepechard@gmail.com> ## ## ## ## OsmUserStats is free software: you can redistribute it and/or modify ## ## it under the terms of the GNU General Public License as published by ## ## the Free Software Foundation, either version 3 of the License, or ## ## (at your option) any later version. ## ## ## ## OsmUserStats is distributed in the hope that it will be useful, ## ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## ## GNU General Public License for more details. ## ## ## ## You should have received a copy of the GNU General Public License ## ## along with OsmUserStats. If not, see <http://www.gnu.org/licenses/>. ## ## ## ########################################################################### """ OsmUserStats is an OSM statistics generator for individual users. You'll need matplotlib to generate graphs. If you cannot generate them, you can ask me to do it for you. For usage and a list of options, try this: $ python OsmUserStats.py -h This program lives here: http://gitorious.org/osmuserstats/osmuserstats WARNING: This work is in progress, NO optimisation has been done yet! Please, stay gentle with OSM database. If you made a lot of changesets, don't fetch them with OsmUserStats too often! """ ########################################################################### ## History ## ########################################################################### ## 0.3.2 OO major refactoring + pylint-validated cleaning code (9.81/10) ## ## 0.3.1 some cleaning ## ## 0.3 option to select the date when OUS begins to take changesets ## ## 0.2 first release, generate first statistics and graphs ## ## 0.1 first version, get data and create database ## ########################################################################### import codecs, httplib, os, re, sqlite3, urlparse, urllib, xml.dom.minidom from optparse import OptionParser from datetime import datetime from pylab import rc, array, append, figure, title, savefig, xticks from pylab import bar, hist, plot_date, xlabel, ylabel from pylab import YearLocator, MonthLocator, DateFormatter __version__ = '0.3.2' print("OsmUserStats " + __version__ + " (C) 2009-2010 Stéphane Péchard") ########################################################################### ## classes ## ########################################################################### class User: """ Class representing an OSM user """ def __init__(self, user_name, app_options): """ Initiates object values and get some basic data. """ self.app_options = app_options self.name = user_name self.page = self.get_page() self.edit_page = self.get_edit_page() self.id_number = self.get_id() self.image_url = None self.mapper_since = self.get_mapper_since() if self.app_options.verbose: print("[INFO] User id of " + self.name + " is: " + self.id_number) def get_page(self): """ Get OSM user info page. """ request = "http://www.openstreetmap.org/user/" + self.name try: sock = urllib.urlopen(url_fix(request)) page_html_source = sock.read() sock.close() except IOError: print("[ERROR] User page unreachable!") page_html_source = None return page_html_source def get_edit_page(self): """ Get OSM user editions page. """ request = "http://www.openstreetmap.org/user/" + self.name + "/edits" try: sock = urllib.urlopen(url_fix(request)) edit_page_html_source = sock.read() sock.close() except IOError: print("[ERROR] User page unreachable!") edit_page_html_source = None return edit_page_html_source def get_id(self): """ Get OSM user id. """ id_number = None self.edit_page = self.edit_page last_changeset = self.get_last_changeset() api_changeset = "http://www.openstreetmap.org/api/0.6/changeset/" changeset_request = api_changeset + last_changeset try: sock = urllib.urlopen(url_fix(changeset_request)) xml_source = sock.read() sock.close() xml_list = xml_source.split('\n') for line in xml_list: if re.search("uid", line): uid_line = line break pattern = \ r'(.*)<changeset id="(.*)" user="(.*)" uid="(.*)" created_at="(\.*)' match_changeset = re.match(pattern, uid_line) id_number = match_changeset.group(4) except IOError: print("[ERROR] User page unreachable!") return id_number def get_last_changeset(self): """ Get last changeset of the editions page. """ edit_page_list = self.edit_page.split('\n') for line in edit_page_list: if re.search("<p>Recent changes</p>", line): raise UnknownUserError('Unknown user') if re.search("View changeset details", line): changeset_line = line break pattern = r'(.*)#<a href="/browse/changeset/(.*)" title=(\.*)' match_line = re.match(pattern, changeset_line) if match_line: last_changeset = match_line.group(2) else: last_changeset = None return last_changeset def get_mapper_since(self): """ Get OSM user inscription date from info page. """ user_page_list = self.page.split('\n') mapper_since_line = None user_image_line = None for line in user_page_list: if re.search("Mapper since:", line): mapper_since_line = line if re.search('<img alt=', line): user_image_line = line # we stop when we got both if (mapper_since_line and user_image_line): break m_mapper_since = re.match(r'<p><b>Mapper since:</b> (.*) \(.*\)</p>', \ mapper_since_line) mapper_since = osm_date_to_iso(m_mapper_since.group(1)) m_user_image = re.match(r'.*<img alt=".*" .* src="(.*)" .*/>', \ user_image_line) self.image_url = 'http://www.openstreetmap.org/' + m_user_image.group(1) if not self.image_url: self.image_url = 'http://www.openstreetmap.org/images/osm_logo.png' return mapper_since def get_data(self, connection): """ Gets all needed data from the OpenStreetMap site and API. """ uid = self.id_number uri = "/api/0.6/changesets?user=" + self.id_number last_changesets = _http_request('GET', uri, connection) # parsing is_last_changeset_found = False chset_list = [] data = xml.dom.minidom.parseString(last_changesets) nodelist = data.getElementsByTagName("changeset") for item in nodelist: changeset = self.create_changeset(item) chset_list.append(changeset) if int(changeset[0]) == -1: if self.app_options.verbose: print("[INFO] Found last changeset in database, stop here!") is_last_changeset_found = True break if not is_last_changeset_found: while nodelist.length == 100: # while there are changesets to get oldest_chset = nodelist[nodelist.length - 1] date_oldest_chset = oldest_chset.getAttribute("created_at") uri = "/api/0.6/changesets?user=" + uid + "&time=" \ + self.app_options.start_changeset_date + "T00:00:00Z," \ + date_oldest_chset old_changesets = _http_request('GET', uri, connection) data = xml.dom.minidom.parseString(old_changesets) nodelist = data.getElementsByTagName("changeset") for item in nodelist: changeset = self.create_changeset(item) chset_list.append(changeset) if int(changeset[0]) == -1: is_last_changeset_found = True break if self.app_options.verbose: print("[INFO] " + str(len(chset_list)) + " changeset(s) processed") return chset_list @classmethod def create_changeset(cls, item): """ Create and return changeset object containing OSM's XML-formatted info. """ try: created_by_tag = item.getElementsByTagName("tag")[0] created_by = created_by_tag.getAttribute("v") comment_tag = item.getElementsByTagName("tag")[1] comment = comment_tag.getAttribute("v") except IndexError: created_by = '' comment = '' try: created_at = datetime.strptime(item.getAttribute("created_at"), "%Y-%m-%dT%H:%M:%SZ"), closed_at = datetime.strptime(item.getAttribute("closed_at"), "%Y-%m-%dT%H:%M:%SZ"), except ValueError: created_at = [0] closed_at = [0] changeset = [ item.getAttribute("id"), item.getAttribute("user"), item.getAttribute("uid"), created_at[0], closed_at[0], item.getAttribute("open"), item.getAttribute("min_lon"), item.getAttribute("min_lat"), item.getAttribute("max_lon"), item.getAttribute("max_lat"), created_by, comment] return changeset class Database: """ Database class """ def __init__(self, osm_user, app_options, db_name, dest_directory): """ initiates object values """ self.app_options = app_options self.name = db_name self.user = osm_user self.directory = dest_directory self.is_closed = True self.last_id = None self.database = None def create_directory(self): """ Creates the working directory if it does not exist already, then enters. Gets the last changeset of the database if it exists. """ if os.path.exists(self.directory): os.chdir(self.directory) if self.app_options.verbose: print("[INFO] Destination directory already exists, \ let's update data!") if os.path.exists(self.name): if self.app_options.remove_db: if self.app_options.verbose: print("[INFO] Removing database file!") os.remove(self.name) else: self.database = sqlite3.connect(self.name) self.is_closed = False cur = self.database.cursor() cur.execute('SELECT id FROM changeset \ ORDER BY id DESC LIMIT 1') result = cur.fetchone() try: self.last_id = result[0] except TypeError: pass else: os.mkdir(self.directory) os.chdir(self.directory) def insert(self, changeset_list): """ Writes/updates database. """ if self.is_closed: self.database = sqlite3.connect(self.name) self.database.execute('''CREATE TABLE IF NOT EXISTS changeset (id INTEGER PRIMARY KEY, user TEXT, uid NUMERIC, created_at DATETIME, closed_at DATETIME, open BOOLEAN, max_lat REAL, max_lon REAL, min_lat REAL, min_lon REAL, created_by TEXT, comment TEXT)''') # insert changeset list items i = 0 for changeset in changeset_list: try: self.database.execute('INSERT INTO changeset VALUES \ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', changeset) except sqlite3.IntegrityError: i = i + 1 self.database.commit() if self.app_options.verbose: print("[INFO] " + str(len(changeset_list) - i) + " changeset(s) \ updated in the database") def get_changesets(self): """ Puts data we need in a list. """ cur = self.database.cursor() cur.execute('SELECT id, created_at, max_lat, max_lon, min_lat, \ min_lon, created_by, comment FROM changeset ORDER BY created_at') return cur.fetchall() class StatsPage: """ HTML page to be created and used by the user to visualized stats. """ def __init__(self, osm_user, app_options, all_changeset): """ initiates object values """ self.user = osm_user self.app_options = app_options self.changesets = all_changeset self.filename = 'index.html' self.lat_mean = None self.lon_mean = None self.nb_changeset = len(self.changesets) def generate(self): """ Constructs page. """ page_content = self.create_header() page_content += self.make_general_section() page_content += self.make_time_section() page_content += self.make_map_section() page_content += self.make_creator_section() page_content += self.create_footer() page_file = codecs.open(self.filename, 'w', encoding='utf-8') page_file.write(page_content) page_file.close() print("[INFO] page 'index.html' created!") def make_general_section(self): """ Constructs the 'General' item of the page. """ date_first_changeset = self.changesets[0][1] date_last_changeset = self.changesets[self.nb_changeset - 1][1] content = ''' <h2>General</h2> <img id="user_image" alt="''' + self.user.name + '''" src="''' \ + self.user.image_url + '''" /> <dl> <dt>User</dt> <dd><a href="http://www.openstreetmap.org/user/''' \ + self.user.name + '''">''' + self.user.name + '''</a></dd>''' if self.user.mapper_since: content += ''' <dt>Mapper since</dt><dd>''' + self.user.mapper_since + ''' (''' \ + pretty_date(datetime.strptime(self.user.mapper_since, \ "%Y-%m-%d %H:%M")) + ''')</dd>''' if date_first_changeset: content += ''' <dt>First changeset</dt><dd>''' + date_first_changeset + ''' (''' \ + pretty_date(datetime.strptime(date_first_changeset, \ "%Y-%m-%d %H:%M:%S")) + ''')</dd>''' if date_last_changeset: content += ''' <dt>Last changeset</dt><dd>''' + date_last_changeset + ''' (''' \ + pretty_date(datetime.strptime(date_last_changeset, \ "%Y-%m-%d %H:%M:%S")) + ''')</dd>''' content += ''' <dt>Number of changeset</dt><dd>''' + unicode(self.nb_changeset) \ + '''</dd></dl>''' return content def make_time_section(self): """ Constructs the 'Time' item of the page. """ # extracts data from changesets date_list, dates_val, hour_array, day_array = self.create_time_arrays() # changeset count by date stat_per_day_name = 'statPerDay.' + self.app_options.image_format self.create_stat_per_day(stat_per_day_name, date_list, dates_val) content = ''' <h2>Time</h2> <h3>Changeset count by date</h3> <p class="graph"><img src="''' + stat_per_day_name + '''" /></p> ''' # day of week th_day, th_day_changeset, th_day_pourcent, stat_day_week_name = \ self.create_day_of_week(day_array) content += ''' <h3>Day of week</h3> <table> <tr><th>Day</th>''' + th_day + '''</tr> <tr><th>Changesets</th>''' + th_day_changeset + '''</tr> <tr><th>%</th>''' + th_day_pourcent + '''</tr> </table> <p class="graph"><img src="''' + stat_day_week_name + '''" /></p> ''' # hour of day th_hour, th_changeset, th_pourcent, stat_hour_day_name = \ self.create_hour_of_day(hour_array) content += ''' <h3>Hour of day</h3> <p>Changesets time is based on OpenStreetMap server time. It may differ from your local time. Use the -t option to adjust it.</p> <table> <tr><th>Hour</th>''' + th_hour + '''</tr> <tr><th>Changesets</th>''' + th_changeset + '''</tr> <tr><th>%</th>''' + th_pourcent + '''</tr> </table> <p class="graph"><img src="''' + stat_hour_day_name + '''" /></p> ''' return content def create_time_arrays(self): """ Constructs the 'Time' arrays. """ hour_array = array([], int) day_array = array([], int) dates_val = [] date_list = [] date_index = -1 for changeset in self.changesets: try: changeset_date_time = datetime.strptime(changeset[1], \ "%Y-%m-%d %H:%M:%S") hour_array = append(hour_array, changeset_date_time.hour + \ self.app_options.timeshift) day_array = append(day_array, changeset_date_time.weekday()) changeset_date = changeset_date_time.date() if changeset_date in date_list: dates_val[date_index] += 1 else: date_list.append(changeset_date) dates_val.append(0) date_index += 1 except TypeError: pass return date_list, dates_val, hour_array, day_array def create_stat_per_day(self, stat_per_day_name, date_list, dates_val): """ Writes on disk the graphic file plotting per-day changesets of the user """ figure_axis = figure().gca() plot_date(date_list, dates_val, '-', linewidth=0.5, marker='o', \ color='#1d242c') figure_axis.xaxis.set_major_locator(YearLocator()) figure_axis.xaxis.set_major_formatter(DateFormatter('%Y')) figure_axis.xaxis.set_minor_locator(MonthLocator()) figure_axis.xaxis.set_minor_formatter(DateFormatter('%b')) for tick_labels in figure_axis.get_xticklabels(True): tick_labels.set_fontsize(7.) for tick_labels in figure_axis.get_xticklabels(): tick_labels.set_y(-0.03) title('Per-day changesets - User: ' + self.user.name) ylabel('Number of changeset') savefig(stat_per_day_name, transparent=True) def create_day_of_week(self, day_array): """ Constructs the day-of-week part of the 'Time' section of the page. """ stat_day_week_name = 'statDayWeek.' + self.app_options.image_format week_day_label = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] pos = range(7) for index in pos: pos[index] += 0.5 day_hist_nb, day_hist_bins, day_position = hist(day_array, 7, \ facecolor='#1d242c') figure() bar(range(7), day_hist_nb, color='#1d242c', width=1, linewidth=0) xticks(pos, week_day_label) title('Day of week - User: ' + self.user.name) ylabel('Number of changeset') savefig(stat_day_week_name, transparent=True) i = 0 th_day = th_day_changeset = th_day_pourcent = '' for day in week_day_label: th_day += '<th>' + day + '</th>' th_day_changeset += '<td>' + str(day_hist_nb[i]) + '</td>' percent = (100 * day_hist_nb[i]) / float(self.nb_changeset) th_day_pourcent += '<td>%.2f</td>' % percent i += 1 return th_day, th_day_changeset, th_day_pourcent, stat_day_week_name def create_hour_of_day(self, hour_array): """ Constructs the hour-of-day part of the 'Time' section of the page. """ stat_hour_day_name = 'statHourDay.' + self.app_options.image_format pos = range(min(hour_array), max(hour_array) + 1) hour_hist_nb, hour_hist_bins, day_position = \ hist(hour_array, max(hour_array) - min(hour_array) + 1, \ facecolor='#1d242c') figure() bar(pos, hour_hist_nb, color='#1d242c', width=1, linewidth=0) title('Hour of day - User: ' + self.user.name) xlabel('Hour') ylabel('Number of changeset') tmp = range(min(hour_array), max(hour_array) + 1) for index in tmp: pos[index - tmp[0]] += 0.5 xticks(pos, tmp) savefig(stat_hour_day_name, transparent=True) i = 0 th_hour = th_changeset = th_pourcent = '' for hour in range(min(hour_array), max(hour_array) + 1): th_hour += '<th>' + str(int(hour)) + '</th>' th_changeset += '<td>' + str(hour_hist_nb[i]) + '</td>' percent = (100 * hour_hist_nb[i]) / float(self.nb_changeset) th_pourcent += '<td>%.2f</td>' % percent i += 1 return th_hour, th_changeset, th_pourcent, stat_hour_day_name def make_map_section(self): """ Constructs the 'Map' section of the page. """ if self.app_options.map: max_lat_mean = max_lon_mean = min_lat_mean = min_lon_mean = 0.0 nb_lat_lon = 0 poisfile = codecs.open('OsmUserStats-pois.txt', 'w', \ encoding='utf-8') poisfile.write('lat\tlon\ttitle\tdescription\ticon\n') # a changeset is made of: 0. int 1. unicode 2. float 3.float # 4. float 5. float 6. unicode 7. unicode for chgset in self.changesets: # only if coordinates are here if(chgset[2] and chgset[3] and chgset[4] and chgset[5]): max_lon_mean += chgset[2] max_lat_mean += chgset[3] min_lon_mean += chgset[4] min_lat_mean += chgset[5] lon_str = str((chgset[4] + chgset[2]) / 2) lat_str = str((chgset[5] + chgset[3]) / 2) mark_title = 'Changeset <a ' \ + 'href="http://www.openstreetmap.org/browse/changeset/' \ + str(chgset[0]) + '">#' + str(chgset[0]) + '</a>' if not chgset[7]: mark_desc = '[No comment]' else: mark_desc = 'Comment: ' + chgset[7] icon = '\thttp://www.openstreetmap.org/openlayers/img/' \ + 'marker.png\n' poisfile.write(lat_str + '\t' + lon_str + '\t' + mark_title + '\t' + mark_desc + icon) nb_lat_lon += 1 poisfile.close() if nb_lat_lon != 0: max_lat_mean /= nb_lat_lon max_lon_mean /= nb_lat_lon min_lat_mean /= nb_lat_lon min_lon_mean /= nb_lat_lon self.lat_mean = (min_lat_mean + max_lat_mean) / 2 self.lon_mean = (min_lon_mean + max_lon_mean) / 2 content = ''' <h2>Map</h2><p>If your map contains too many points, your browser may have difficulties to render it. You can disable it not using the -m flag. </p><div style="width:100%; height:60%" id="map"></div> ''' else: content = '' return content def make_creator_section(self): """ Constructs the 'Creator' item of the page. """ creator_dict = {}.fromkeys( ['almien_coastline', 'coastbot', 'dmgroom_coastlines', 'Editop', 'FindvejBot', 'fixbot', 'gis2osm', 'GPSBabel', 'JOSM', 'ktj_abq_gis_addr_convert', 'Merkaartor', 'mumpot', 'National-Land-Numerical-Information_MLIT_Japan', 'opengeodb2osm', 'osmeditor2', 'OSMNavigator', 'osm2go', 'POI Editor', 'polyshp2osm', 'Potlatch', 'r_coastlines', 'shpupload', 'shp2osm', 'srtm_coastline', 'TCX2OSM-JN', 'TRIPLER', 'xybot', 'YahooApplet'], 0) stat_creator_name = 'statCreator.' + self.app_options.image_format # filling the dictionary for changeset in self.changesets: for key in creator_dict: if re.search(key, changeset[6]): creator_dict[key] += 1 break # keeping only used ones total_creator = 0 creator_used = [] for item in creator_dict.items(): if item[1]: total_creator += item[1] tmp = list(item) tmp.reverse() creator_used.append(tuple(tmp)) #creator_used.append(tuple(list(item).reverse())) # sorting and listing them creator_val = array([], int) creator_label = [] for item in creator_used: creator_val = append(creator_val, int(item[0])) creator_label.append(item[1]) if total_creator != self.nb_changeset: creator_val = append(creator_val, self.nb_changeset - total_creator) creator_label.append('Unknown') th_creator, th_creator_changeset, th_creator_pourcent = \ self.create_creator_graphs(creator_val, creator_label, \ stat_creator_name) content = ''' <h2>Creator</h2> <p>The most common OpenStreetMap data creators are <a href="http://www.merkaartor.org/">Merkaartor</a>, <a href="http://josm.openstreetmap.de/">JOSM</a> and <a href="http://potlatchosm.wordpress.com/">Potlatch</a>, but you may use others. As creators are in changesets only since version 0.6 of the API, you may have a high value for "unknown" creator.</p> <p><table> <tr><th>Creator</th>''' + th_creator + '''</tr> <tr><th>Changesets</th>''' + th_creator_changeset + '''</tr> <tr><th>%</th>''' + th_creator_pourcent + '''</tr> </table> <img alt="OSM data creators used by ''' + stat_creator_name \ + '''" src="''' + stat_creator_name + '''" /></p> ''' return content def create_creator_graphs(self, creator_val, creator_label, stat_creator): """ Constructs the Creator graphs of the page. """ figure() pos = range(len(creator_val)) bar(pos, creator_val, color='#1d242c', linewidth=0, align='center') xticks(pos, creator_label) ylabel('Number of changeset') title('OSM data creators - User: ' + self.user.name) savefig(stat_creator, transparent=True) th_creator = th_creator_changeset = th_creator_pourcent = '' for index in range(len(creator_val)): th_creator += '<th>' + creator_label[index] + '</th>' th_creator_changeset += '<td>' + str(creator_val[index]) + '</td>' percent = (100 * creator_val[index]) / float(len(self.changesets)) th_creator_pourcent += '<td>%.2f</td>' % percent return th_creator, th_creator_changeset, th_creator_pourcent def create_header(self): """ Constructs page header. """ header = '''<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head><title>OsmUserStats for ''' + self.user.name + '''</title> <meta http-equiv="Content-Type" content="charset=utf-8" /> <meta http-equiv="Content-language" content="en" /> <meta name="generator" content="OsmUserStats" /></head> <style type="text/css" media="all"> html, body{ background: #f9f8f7; color: #323231; margin: 0; padding: 0; font: 90% Helvetica, Arial, 'Liberation Sans', FreeSans, sans-serif; } .content{ margin: 0 auto;max-width: 900px; } .header, .footer{ background: #1d242c; color: #f9f8f7; } .footer{ text-align: right; } .footer a{ font-weight: bold; } h1, .footer{ padding:5px; margin:0; } h1 a:link, h1 a:visited, h1 a:active, .footer a:link, .footer a:visited, .footer a:active { text-decoration: none; color: #f9f8f7; } h1 a:hover, .footer a:hover { text-decoration: underline; } h2{ padding-top:20px; margin:0; text-align: right; border-bottom:1px #ccc solid; } #USER_IMAGE { float: right; } p{ margin: 0; padding: 0; margin-bottom: 1em; } p.graph { text-align: center; } dt{ font-weight: bold; float: left; margin-right: 1em; } dt:after{ content: ':'; } dd{ display: block; clear: left; } table{ border-collapse: collapse; margin-left: auto; margin-right: auto; font-size: 100%; } th { background-color: #1d242c; color: #f9f8f7; padding: 0.2em 0.3em; text-align: center; } td { border-top: 1px solid black; padding: 0.2em 0.3em; text-align: center; } </style>''' if self.app_options.map: header += ''' <script src="http://www.openlayers.org/api/OpenLayers.js"></script> <script src="http://www.openstreetmap.org/openlayers/OpenStreetMap.js"> </script> <script type="text/javascript"> // Start position for the map var meanLat=''' + str(self.lat_mean) + ''' var meanLon=''' + str(self.lon_mean) + ''' var zoom=10 var map; function init() { map = new OpenLayers.Map ("map", { controls:[ new OpenLayers.Control.Navigation(), new OpenLayers.Control.PanZoomBar(), new OpenLayers.Control.LayerSwitcher(), new OpenLayers.Control.Attribution()], maxExtent: new OpenLayers.Bounds(-20037508.34,-20037508.34, 20037508.34,20037508.34), maxResolution: 156543.0399, numZoomLevels: 19, units: 'm', projection: new OpenLayers.Projection("EPSG:900913"), displayProjection: new OpenLayers.Projection("EPSG:4326") } ); layerMapnik = new OpenLayers.Layer.OSM.Mapnik("Mapnik"); layerTilesAtHome = new OpenLayers.Layer.OSM.Osmarender("Osmarender"); layerMarkers = new OpenLayers.Layer.Markers("Markers"); map.addLayers([layerMapnik, layerTilesAtHome, layerMarkers]); var icon = new OpenLayers.Icon( 'http://www.openstreetmap.org/openlayers/img/marker-green.png'); // center (mean of all changesets position) var lonLat = new OpenLayers.LonLat(meanLon, meanLat).transform(new OpenLayers.Projection("EPSG:4326"), map.getProjectionObject()); layerMarkers.addMarker(new OpenLayers.Marker(lonLat,icon)); map.setCenter (lonLat, zoom); // all changesets positions var pois = new OpenLayers.Layer.Text("My changesets' mean positions", { location:"./OsmUserStats-pois.txt", projection: map.displayProjection }); map.addLayer(pois); } </script><body onload="init();">''' else: header += '''<body>''' header += ''' <h1 class="header"> <a href="http://spechard.dgplug.org/OsmUserStats/">OsmUserStats</a></h1> <div class="content"> ''' return header @classmethod def create_footer(cls): """ Constructs page footer. """ return '''</div><div class="footer">Generated by <a href="http://spechard.dgplug.org/OsmUserStats/">OsmUserStats</a> on ''' \ + str(datetime.now().strftime("%A %d %B %Y at %H:%M")) \ + '''</div></body></html>''' ########################################################################### ## error classes ## ########################################################################### class UnknownUserError(Exception): """Exception raised when user name given in arguments does not existe.""" pass ########################################################################### ## utility functions ## ########################################################################### def pretty_date(time=False): """ Gets a datetime object or a int() Epoch timestamp and return a pretty string like 'an hour ago', 'Yesterday', '3 months ago', 'just now', etc """ # constructing diff from now to the given date now = datetime.now() diff = now - now if type(time) is int: diff = now - datetime.fromtimestamp(time) elif type(time) is datetime: diff = now - time second_diff = diff.seconds day_diff = diff.days # init at worst case pretty_date_string = str(day_diff / 365) + " years ago" if not day_diff: # less than a day ago if second_diff < 10: pretty_date_string = "just now" elif second_diff < 60: pretty_date_string = str(second_diff) + " seconds ago" elif second_diff < 120: pretty_date_string = "a minute ago" elif second_diff < 3600: pretty_date_string = str(second_diff / 60) + " minutes ago" elif second_diff < 7200: pretty_date_string = "an hour ago" elif second_diff < 86400: pretty_date_string = str(second_diff / 3600) + " hours ago" elif day_diff == 1: # more than a day ago pretty_date_string = "yesterday" elif day_diff < 7: pretty_date_string = str(day_diff) + " days ago" elif day_diff < 31: pretty_date_string = str(day_diff / 7) + " weeks ago" elif day_diff < 365: pretty_date_string = str(day_diff / 30) + " months ago" return pretty_date_string def _http_request(cmd, path, connection): """ Processes a HTTP request. """ connection.request(cmd, path) response = connection.getresponse() if response.status != 200: response.read() if response.status == 410: return None return response.read() def osm_date_to_iso(osm_date): """ Transforms an OSM-formatted date to iso format. """ m_date = re.match(r'(\d{2}) ([^ ]+) (\d{4}) at (\d{1,2}:\d{2})', osm_date) months = {'January': '01', 'February': '02', 'March': '03', 'April': '04', 'May': '05', 'June': '06', 'July': '07', 'August': '08', 'September': '09', 'October': '10', 'November': '11', 'December': '12'} month = months[m_date.group(2)] iso = m_date.group(3) + '-' + month + '-' + m_date.group(1) + ' ' \ + m_date.group(4) return iso def url_fix(string, charset='utf-8'): """Sometimes you get an URL by a user that just isn't a real URL because it contains unsafe characters like ' ' and so on. This function can fix some of the problems in a similar way browsers handle data entered by users: >>> url_fix(u'http://de.wikipedia.org/wiki/Elf (Begriffsklärung)') 'http://de.wikipedia.org/wiki/Elf%20%28Begriffskl%C3%A4rung%29' :param charset: The target charset for the URL if the url was given as unicode string. """ if isinstance(string, unicode): string = string.encode(charset, 'ignore') scheme, netloc, path, qstring, anchor = urlparse.urlsplit(string) path = urllib.quote(path, '/%') qstring = urllib.quote_plus(qstring, ':&=') return urlparse.urlunsplit((scheme, netloc, path, qstring, anchor)) def process_options(arglist=None): """ Processes options passed either via arglist or via command line args. to be updated by http://docs.python.org/library/argparse.html which is available with python 2.7 """ default_image_format = 'png' start_changeset_date_default = '2009-04-21' parser = OptionParser(usage="%prog [options] \"username\"") parser.add_option("-m", "--map", action="store_true", dest="map", default=False, help="generates a map section, with \ changesets positions (heavy for the browser)") parser.add_option("-t", "--timeshift", dest="timeshift", default=0, type="int", metavar="TIMESHIFT", help="hour difference \ between your location and the OSM server, integers only \ [default: %default]") parser.add_option("-d", "--date", dest="start_changeset_date", metavar="DATE", default=start_changeset_date_default, help="date from which data gathering begins [default: \ %default, date of API 0.6, introducing changesets]") parser.add_option("-r", "--removeDB", action="store_true", dest="remove_db", default=False, help="removes the \ database file before fetching data") parser.add_option("-f", "--format", dest="image_format", default=default_image_format, metavar="FMT", help="image format: svg or png [default: %default]") parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="prints everything") (options, args) = parser.parse_args(arglist) if len(args) != 1: parser.error("wrong number of arguments") return options, args def _main(): """ Parses options and does the job! """ app_options, app_args = process_options() try: osm_user = User(app_args[0].decode('utf-8'), app_options) except IOError or UnknownUserError: exit() # gets data connection = httplib.HTTPConnection('www.openstreetmap.org', 80) changeset_list = osm_user.get_data(connection) # creates dir and db dest_directory = 'OsmUserStats-' + osm_user.name db_name = 'OsmUserStats.sqlite' app_database = Database(osm_user, app_options, db_name, dest_directory) # then populates it if necessary app_database.create_directory() app_database.insert(changeset_list) # gets data to be plotted all_changeset = app_database.get_changesets() # matplot config for all graphs rc('font', **{'weight': 'bold', 'size': 10}) # creates final page stats_page = StatsPage(osm_user, app_options, all_changeset) stats_page.generate() if __name__ == '__main__': _main()