#!/usr/bin/env python
#
# osmoo v0.1 (27-nov-2007)
#
# See usage in code, below, or run with -h.
#
# http://api.openstreetmap.org/api/0.5/
#
#   /node/<id>
#   /node/<id>/history
#   /node/<id>/ways
#   /node/<id>/relations
#
#   /way/<id>
#   /way/<id>/history
#   /way/<id>/relations
#   /way/<id>/full
#
#   /relation/<id>/history
#   /relation/<id>/relations
#   /relation/<id>/full
#
#   /ways?ways=<id>[,<id>...]
#   /changes?hours=h&zoom=z&start=t&end=e
#   /nodes?nodes=<id>[,<id>...]
#   /relations?relations=<id>[,<id>...]
#
#   /ways/search?type=<type>&value=<value>
#   /nodes/search?type=<type>&value=<value>
#   /relations/search?type=<type>&value=<value>
#   /search/search?type=<type>&value=<value>
#
#   /map?bbox=<left>,<bottom>,<right>,<top>
#   /trackpoints?bbox=<left>,<bottom>,<right>,<top>&page=<page>
#
# http://www.informationfreeway.org/api/0.5/
#
#   /node[<query>][<bbox>]
#   /way[<query>][<bbox>]
#
# When deleting, ways are deleted before nodes.  When creating, nodes
# are created before ways.
#
# Beej's playground:
#   --bbox=-121.613989,41.716404,-121.604626,41.722451
#
#-----------------------------------------------------------------------
# Copyright (c) 2007 Brian "Beej Jorgensen" Hall <beej@beej.us>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

import sys
import getopt
import os.path
import getpass
import fcntl
import urllib2
import xml.dom.minidom
import base64

#-------------------------------------------------------------------------
class AppContext(object):
    apiurl  = 'http://api.openstreetmap.org/api/0.5/'
    xapiurl = 'http://www.informationfreeway.org/api/0.5/'

    osmrealm = ''
    osmhost = apiurl

    MODE_GET = 1
    MODE_PUT = 2
    MODE_CREATE = 3
    MODE_UPDATE = 4
    MODE_DELETE = 5

    OPERAND_NONE = 0
    OPERAND_NODE = 1
    OPERAND_WAY = 2
    OPERAND_RELATION = 4

    MODIFIER_FULL = 1
    MODIFIER_RELATION = 2
    MODIFIER_HISTORY = 3
    MODIFIER_WAYS = 4

    def __init__(self, argv):
        self.scriptname = os.path.basename(argv[0])

        self.nidlist = None
        self.widlist = None
        self.mode = None
        self.bbox = None
        self.usexapi = False
        self.xquery = None
        self.username = None
        self.password = None
        self.operand = self.OPERAND_NONE
        self.modifier = None
        self.outfilename = '-'
        self.debuglevel = 0
        self.quiet = False
        self.osmfilename = None

        self.envusername = '%s_USERNAME' % self.scriptname.upper()
        self.envpassword = '%s_PASSWORD' % self.scriptname.upper()

        try:
            opts, args = getopt.gnu_getopt(argv[1:], 'hi:gpcudxq:nwro:', \
                    ['help', 'id=', 'get', 'put', 'create', 'update', \
                    'delete', 'username=', 'password=', 'xapi', \
                    'xquery=', 'full', 'relations', 'history', \
                    'ways', 'nid=', 'wid=', 'outfile=', 'bbox=', \
                    'debug=', 'quiet'])
        except getopt.GetoptError:
            self.usageExit()

        commandErrStr = "specify only one of -g, -d, -p, -c, or -u"

        for o,a in opts:
            if o in ('-h', '--help'):
                self.usageExit()

            if o == '--debug':
                try:
                    self.debuglevel = int(a)
                except:
                    self.usageExit("debuglevel must be integer")
        
            elif o  == '--quiet':
                self.quiet = True

            elif o in ('-g', '--get'):
                if self.mode != None and self.mode != ac.MODE_GET:
                    self.usageExit(commandErrStr)
                self.mode = self.MODE_GET

            elif o in ('-p', '--put'):
                if self.mode != None and self.mode != ac.MODE_PUT:
                    self.usageExit(commandErrStr)
                self.mode = self.MODE_PUT

            elif o in ('-c', '--create'):
                if self.mode != None and self.mode != ac.MODE_CREATE:
                    self.usageExit(commandErrStr)
                self.mode = self.MODE_CREATE

            elif o in ('-u', '--update'):
                if self.mode != None and self.mode != ac.MODE_UPDATE:
                    self.usageExit(commandErrStr)
                self.mode = self.MODE_UPDATE

            elif o in ('-d', '--delete'):
                if self.mode != None and self.mode != ac.MODE_DELETE:
                    self.usageExit(commandErrStr)
                self.mode = self.MODE_DELETE

            elif o in ('-o', '--outfile'):
                self.outfilename = a

            elif o == '--username':
                self.username = a

            elif o == '--password':
                self.password = a

            elif o == '--bbox':
                self.bbox = a.split(',')
                if len(self.bbox) != 4:
                    self.usageExit('bounding box requires lon1,lat1,lon2,lat2')

                self.bbox[0] = float(self.bbox[0])
                self.bbox[1] = float(self.bbox[1])
                self.bbox[2] = float(self.bbox[2])
                self.bbox[3] = float(self.bbox[3])

                # correct order, lower left to upper right:
                if self.bbox[0] > self.bbox[2]:
                    temp = self.bbox[0]
                    self.bbox[0] = self.bbox[2]
                    self.bbox[2] = temp
                if self.bbox[1] > self.bbox[3]:
                    temp = self.bbox[1]
                    self.bbox[1] = self.bbox[3]
                    self.bbox[3] = temp

                self.bboxstr = "%f,%f,%f,%f" % (self.bbox[0],
                        self.bbox[1], self.bbox[2], self.bbox[3])

            elif o == '--nid':
                self.nidlist = a.split(',')
                self.operand = self.OPERAND_NODE

            elif o == '--wid':
                self.widlist = a.split(',')
                self.operand = self.OPERAND_WAY

            elif o == '--rid':
                self.ridlist = a.split(',')
                self.operand = self.OPERAND_RELATION

            elif o in ('-x', '--xapi'):
                self.usexapi = True

            elif o in ('-q', '--xquery'):
                self.xquery = a
                self.usexapi = True

            elif o == '-n':
                self.operand = self.OPERAND_NODE

            elif o == '-w':
                self.operand = self.OPERAND_WAY

            elif o == '-r':
                self.operand = self.OPERAND_RELATION

            elif o == '--full':
                self.modifier = self.MODIFIER_FULL

            elif o == '--relation':
                self.modifier = self.MODIFIER_RELATION

            elif o == '--history':
                self.modifier = self.MODIFIER_HISTORY

            elif o == '--ways':
                self.modifier = self.MODIFIER_WAYS

        # get osmfilename
        if len(args) == 1:
            self.osmfilename = args[0]
        elif len(args) != 0:
            self.usageExit()

        # only MODE_GET allowed with xapi
        if self.usexapi:
            if self.mode != None:
                if self.mode != self.MODE_GET:
                    self.usageExit("XAPI can only be used with -g")
            else:
                self.mode = self.MODE_GET

        if self.mode == None:
            self.usageExit()

        # check for bad combinations of options
        if self.modifier == self.MODIFIER_FULL and \
                (self.operand != self.OPERAND_WAY and \
                self.operand != self.OPERAND_RELATION):
            self.usageExit("full modifier can only be used with ways or relations")

        if self.modifier == self.MODIFIER_WAYS and \
                self.operand != self.OPERAND_NODE:
            self.usageExit("ways modifier can only be used with nodes")

        if self.usexapi and \
                (self.operand != self.OPERAND_NODE and \
                self.operand != self.OPERAND_WAY):
            self.usageExit("must specify nodes or ways with xapi")

        if self.usexapi and self.xquery == None:
            self.usageExit("must specify xquery with xapi")

        if (self.mode == self.MODE_PUT or \
                self.mode == self.MODE_CREATE or \
                self.mode == self.MODE_UPDATE) and \
                self.osmfilename == None:
            self.usageExit("file required for put, update, or create")

        # look for username and password in the environment
        if self.needsWritePriv():
            if os.environ.has_key(self.envusername):
                self.username = os.environ[self.envusername]
            if os.environ.has_key(self.envpassword):
                self.password = os.environ[self.envpassword]

    def usageExit(self, str=None, status=1):
        if str != None:
            sys.stderr.write('%s: %s\n' % (self.scriptname, str))
        else:
            sys.stderr.write( \
"""usage: %s command [options] [osmfile]

  Commands:

   -g  --get                    get information for ids
   -p  --put                    create or update ids
   -c  --create                 create only
   -u  --update                 update only
   -d  --delete                 delete ids

  Data or node numbers come from the osmfile or the -i idlist (-g, -d
  only).  For get or update, nodes must have a positive id.


  Options:

   -o  --outfile=outfilename    set output file name
   -n                           perform operations on nodes
   -w                           perform operations on ways
   -r                           perform operations on relations
       --full                   do a full lookup on a way or relation
       --relations              do a relations lookup
       --history                do a history lookup
       --ways                   do a ways lookup on a node
       --nid=id1[,id2...]       use node id list instead of osmfile (-g|d)
       --wid=id1[,id2...]       use waypoint id list instead of osmfile (-g|d)
       --rid=id1[,id2...]       use relation id list instead of osmfile (-g|d)
       --bbox=ln1,lt1,ln2,lt2   specify lon,lat bounding box
       --username=username      use this login name
       --password=password      use this password
   -x  --xapi                   use xapi for access (-g only)
   -q  --xquery=query           specify xapi query string (no [])
   -h  --help                   usage help
       --debug=level            set debug level (higher=more)
       --quiet                    suppress normal output

  If needed and not specified, username and password will be prompted
  for.  They can also be set in the following environment variables:
      
  %s, %s

""" % (self.scriptname, self.envusername, self.envpassword))
        sys.exit(status)

    def warn(self, msg):
        sys.stderr.write('%s: %s\n' % (self.scriptname, msg))

    def debug(self, msg, level=1):
        if level <= self.debuglevel:
            sys.stderr.write('%s: DEBUG: %s\n' % (self.scriptname, msg))

    def info(self, msg, pretext='', nolead=False):
        if not self.quiet:
            if nolead:
                sys.stderr.write(msg)
            else:
                sys.stderr.write('%s%s: %s' % (pretext, self.scriptname, msg))
            sys.stderr.flush()

    def needsWritePriv(self):
        return self.mode == self.MODE_PUT or \
            self.mode == self.MODE_CREATE or \
            self.mode == self.MODE_UPDATE or \
            self.mode == self.MODE_DELETE

#-------------------------------------------------------------------------
# wrapper class to allow changing the HTTP request method (need PUT, DELETE)
#
class osmooRequest(urllib2.Request):
    def __init__(self, url, data=None, headers={}, origin_req_host=None, \
                unverifiable=False, method=None):

        urllib2.Request.__init__(self, url=url, data=data, headers=headers, \
                origin_req_host=origin_req_host, unverifiable=unverifiable)

        self.method = method

        if self.method == None:
            self.method = urllib2.Request.get_method(self)

    def get_method(self):
        return self.method

    def set_method(self, method):
        self.method = method

#-------------------------------------------------------------------------
def managePassword(ac):
    # get username if not specified
    if ac.needsWritePriv():
        if ac.username == None:
            sys.stdout.write('Username: ')
            sys.stdout.flush()
            ac.username = sys.stdin.readline().strip()

        if ac.password == None:
            ac.password = getpass.getpass()

#-------------------------------------------------------------------------
def putOrDeleteURL(ac, mode, url, n):
    authstr = base64.b64encode(':'.join((ac.username, ac.password))).strip()

    if n != None:
        data = '<osm version="0.5" generator="osmoo">%s</osm>' % n.toxml()
    else:
        data = ''

    headers = {'Authorization': 'Basic ' + authstr}

    ac.debug("url: " + url)
    ac.debug("auth: " + authstr)
    ac.debug("data: " + data)

    data = data.encode('utf-8')

    req = osmooRequest(url=url, method=mode, headers=headers, data=data)

    #ac.info('connecting...\n')

    con = urllib2.urlopen(req)

    #ac.info('transferring...\n')

    result = con.read()

    con.close()

    #ac.info('connection closed\n')

    ac.debug("result: " + result)
    return result

#-------------------------------------------------------------------------
def getURL(ac, url):
    ac.debug("url: " + url)

    if ac.outfilename == '-':
        fout = sys.stdout
    else:
        fout = file(ac.outfilename, "w")

    ac.info('connecting...\n')

    con = urllib2.urlopen(url)
    data = con.read(4096)
    total = len(data)
    while data != '':
        #ac.debug('recv %d bytes' % len(data), 5)
        ac.info('recieved %d bytes' % total, pretext='\r')

        fout.write(data)
        data = con.read(4096)
        total += len(data)

    con.close()

    if fout != sys.stdout:
        fout.close()

    ac.info('connection closed\n', pretext='\n')

#-------------------------------------------------------------------------
def get_xapi(ac):
    assert(ac.operand == ac.OPERAND_NODE or ac.operand == ac.OPERAND_WAY)
    assert(ac.xquery != None)

    if ac.bbox != None:
        bboxstr = '[bbox=%s]' % ac.bboxstr
    else:
        bboxstr = ''

    querystr = '[%s]' % ac.xquery

    if ac.operand == ac.OPERAND_NODE:
        opstr = 'node'
    elif ac.operand == ac.OPERAND_WAY:
        opstr = 'way'

    url = "".join((ac.xapiurl, opstr, querystr, bboxstr))

    getURL(ac, url)

#-------------------------------------------------------------------------
def get(ac):
    qtypestr = None
    url = None

    # see if we're just getting for a bbox:
    if ac.bbox != None:
        url = "%smap?bbox=%s" % (ac.apiurl, ac.bboxstr)

    else: # see if the user wants a list of nodes
        if ac.widlist != None:
            idlist = ac.widlist
            qtypestr = "way"
        elif ac.nidlist != None:
            idlist = ac.nidlist
            qtypestr = "node"
        elif ac.ridlist != None:
            idlist = ac.ridlist
            qtypestr = "relation"

        if len(idlist) > 1:  # multiple ids
            if ac.modifier != None:
                ac.warn("ignoring modifier with multiple ids")
                
            url = "%s%ss?%ss=%s" % (ac.apiurl, qtypestr, qtypestr, \
                    ','.join(idlist))
            modstr = ''

        else: # single id
            modstr = ''

            if ac.modifier == ac.MODIFIER_HISTORY:
                modstr = '/history'
            elif ac.modifier == ac.MODIFIER_RELATION:
                modstr = '/relations'
            elif ac.modifier == ac.MODIFIER_FULL:
                modstr = '/full'

            url = "".join((ac.apiurl, qtypestr, '/', idlist[0], modstr))

    getURL(ac, url)

#-------------------------------------------------------------------------
def getSymbolicId(id):
    if id == '': # no id, so we'll never match it, but make up a symb name
        return "45e0f7ea3310091e9b6f04298b81d33b"  # unique random string

    try:
        idint = int(id)
        if idint < 0:
            return id  # id is negative, that's the symbolic version
    except:
        return id  # id is not a number, that's the symbolic version

    return None  # id is a valid number, no symbolic version

#-------------------------------------------------------------------------
def put(ac):
    newNodeMap = {}

    dom = xml.dom.minidom.parse(ac.osmfilename)
    doc = dom.documentElement
    
    doc.setAttribute('generator', ac.scriptname)

    # transfer nodes
    nodelist = doc.getElementsByTagName('node')

    for n in nodelist:
        id = n.getAttribute('id')
        symbolicId = getSymbolicId(id)

        if symbolicId != None:  # need to map to new node
            n.removeAttribute('id')
            mode = 'create'
            printableId = symbolicId
        else:
            mode = id
            printableId = id

        url = "".join((ac.apiurl, 'node/', mode))

        ac.info('transferring node [id=%s]' % printableId)
        result = putOrDeleteURL(ac, "PUT", url, n).strip()
        if symbolicId != None:
            ac.info(' (new id=%s)\n' % result, nolead=True)
            # track new node IDs so we can reference them later for ways
            newNodeMap[symbolicId] = result
            n.setAttribute('id', result)  # modify XML to reflect
        else:
            ac.info('\n')

        ac.debug(str(newNodeMap))

    # transfer ways
    waylist = doc.getElementsByTagName('way')

    for w in waylist:
        bail = False
        id = w.getAttribute('id')
        symbolicId = getSymbolicId(id)

        ndlist = w.getElementsByTagName('nd')
        for nd in ndlist:
            refid = nd.getAttribute('ref')
            if not newNodeMap.has_key(refid):
                ac.warn("ignoring way that references nonexistant symbolic node %s" % refid)
                bail = True
                break

            refid = newNodeMap[refid]
            nd.setAttribute('ref', refid)  # modify XML to reflect

        if bail: break

        if symbolicId != None:  # need to map to new node
            w.removeAttribute('id')
            mode = 'create'
            printableId = symbolicId
        else:
            mode = id
            printableId = id

        url = "".join((ac.apiurl, 'way/', mode))

        ac.info('transferring way [id=%s]' % printableId)
        result = putOrDeleteURL(ac, "PUT", url, w).strip()

        if symbolicId != None:
            ac.info(' (new id=%s)\n' % result, nolead=True)
            # track new node IDs so we can reference them later for ways
            newNodeMap[symbolicId] = result
            w.setAttribute('id', result)  # modify XML to reflect
        else:
            ac.info('\n')

    if ac.outfilename == '-': fp = sys.stdout
    else: fp = file(ac.outfilename, "w")

    fp.write(doc.toprettyxml().encode('utf-8'))

    if fp != sys.stdout: fp.close()

#-------------------------------------------------------------------------
def delete(ac):
    if ac.osmfilename != None:
        dom = xml.dom.minidom.parse(ac.osmfilename)
        doc = dom.documentElement

    else:
        dom = None
    
    # delete ways first, in case some nodes reference them
    if dom != None:
        waylist = doc.getElementsByTagName('way')
    else:
        waylist = ac.widlist
        if waylist == None:
            waylist = []

    for w in waylist:
        if dom != None:
            id = w.getAttribute('id')
        else:
            id = w # get it from the widlist

        ac.info('deleting way [id=%s]\n' % id)
        url = "".join((ac.apiurl, 'way/', id))

        putOrDeleteURL(ac, "DELETE", url, w)

    # delete nodes
    if dom != None:
        nodelist = doc.getElementsByTagName('node')
    else:
        nodelist = ac.nidlist
        if nodelist == None:
            nodelist = []

    for n in nodelist:
        if dom != None:
            id = n.getAttribute('id')
        else:
            id = n # get it from the nidlist

        ac.info('deleting node [id=%s]\n' % id)
        url = "".join((ac.apiurl, 'node/', id))

        putOrDeleteURL(ac, "DELETE", url, None)

#-------------------------------------------------------------------------
def main(argv):
    ac = AppContext(argv)
    managePassword(ac)

    if ac.mode == ac.MODE_GET:
        if ac.usexapi:
            get_xapi(ac)
        else:
            get(ac)

    elif ac.mode == ac.MODE_PUT:
        put(ac)

    elif ac.mode == ac.MODE_DELETE:
        delete(ac)

    else:
        sys.stderr.write("unimplemented mode\n")

    return 0

if __name__ == "__main__": sys.exit(main(sys.argv))
