#!/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()