#!/usr/bin/python # # osmoo v0.1 (27-nov-2007) # # See usage in code, below, or run with -h. # # http://api.openstreetmap.org/api/0.5/ # # /node/ # /node//history # /node//ways # /node//relations # # /way/ # /way//history # /way//relations # /way//full # # /relation//history # /relation//relations # /relation//full # # /ways?ways=[,...] # /changes?hours=h&zoom=z&start=t&end=e # /nodes?nodes=[,...] # /relations?relations=[,...] # # /ways/search?type=&value= # /nodes/search?type=&value= # /relations/search?type=&value= # /search/search?type=&value= # # /map?bbox=,,, # /trackpoints?bbox=,,,&page= # # http://www.informationfreeway.org/api/0.5/ # # /node[][] # /way[][] # # 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 # # 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 = '%s' % 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))