imported OsmApi 0.2.8 from svn.openstreetmap.org
[osm-ro-tools.git] / OsmApi.py
blob5458dfc28957961528305c405a4a17e6f6a039ef
1 #-*- coding: utf-8 -*-
3 ###########################################################################
4 ## ##
5 ## Copyrights Etienne Chové <chove@crans.org> 2009 ##
6 ## ##
7 ## This program is free software: you can redistribute it and/or modify ##
8 ## it under the terms of the GNU General Public License as published by ##
9 ## the Free Software Foundation, either version 3 of the License, or ##
10 ## (at your option) any later version. ##
11 ## ##
12 ## This program is distributed in the hope that it will be useful, ##
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of ##
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ##
15 ## GNU General Public License for more details. ##
16 ## ##
17 ## You should have received a copy of the GNU General Public License ##
18 ## along with this program. If not, see <http://www.gnu.org/licenses/>. ##
19 ## ##
20 ###########################################################################
22 ## HomePage : http://wiki.openstreetmap.org/wiki/PythonOsmApi
24 ###########################################################################
25 ## History ##
26 ###########################################################################
27 ## 0.2.8 2009-10-13 *(Create|Update|Delete) use not unique _do method ##
28 ## 0.2.7 2009-10-09 implement all missing fonctions except ##
29 ## ChangesetsGet and GetCapabilities ##
30 ## 0.2.6 2009-10-09 encoding clean-up ##
31 ## 0.2.5 2009-10-09 implements NodesGet, WaysGet, RelationsGet ##
32 ## ParseOsm, ParseOsc ##
33 ## 0.2.4 2009-10-06 clean-up ##
34 ## 0.2.3 2009-09-09 keep http connection alive for multiple request ##
35 ## (Node|Way|Relation)Get return None when object ##
36 ## have been deleted (raising error before) ##
37 ## 0.2.2 2009-07-13 can identify applications built on top of the lib ##
38 ## 0.2.1 2009-05-05 some changes in constructor -- chove@crans.org ##
39 ## 0.2 2009-05-01 initial import ##
40 ###########################################################################
42 __version__ = '0.2.8'
44 import httplib, base64, xml.dom.minidom, time
46 ###########################################################################
47 ## Main class ##
49 class OsmApi:
51 def __init__(self,
52 username = None,
53 password = None,
54 passwordfile = None,
55 appid = "",
56 created_by = "PythonOsmApi/"+__version__,
57 api = "www.openstreetmap.org",
58 changesetauto = False,
59 changesetautotags = {},
60 changesetautosize = 500,
61 changesetautogroup = False):
63 # Get username
64 if username:
65 self._username = username
66 elif passwordfile:
67 self._username = open(passwordfile).readline().split(":")[0].strip()
69 # Get password
70 if password:
71 self._password = password
72 elif passwordfile:
73 for l in open(passwordfile).readlines():
74 l = l.strip().split(":")
75 if l[0] == self._username:
76 self._password = l[1]
78 # Changest informations
79 self._changesetauto = changesetauto # auto create and close changesets
80 self._changesetautotags = changesetautotags # tags for automatic created changesets
81 self._changesetautosize = changesetautosize # change count for auto changeset
82 self._changesetautogroup = changesetautogroup # group data to upload (NodeCreate, NodeUpdate... will return None)
83 self._changesetautodata = [] # data to upload for auto group
85 # Get API
86 self._api = api
88 # Get created_by
89 if not appid:
90 self._created_by = created_by
91 else:
92 self._created_by = appid + " (" + created_by + ")"
94 # Initialisation
95 self._CurrentChangesetId = 0
97 # Http connection
98 self._conn = httplib.HTTPConnection(self._api, 80)
100 #######################################################################
101 # Capabilities #
102 #######################################################################
104 def Capabilities(self):
105 raise NotImplemented
107 #######################################################################
108 # Node #
109 #######################################################################
111 def NodeGet(self, NodeId, NodeVersion = -1):
112 """ Returns NodeData for node #NodeId. """
113 uri = "/api/0.6/node/"+str(NodeId)
114 if NodeVersion <> -1: uri += "/"+str(NodeVersion)
115 data = self._get(uri)
116 if not data: return data
117 data = xml.dom.minidom.parseString(data)
118 data = data.getElementsByTagName("osm")[0].getElementsByTagName("node")[0]
119 return self._DomParseNode(data)
121 def NodeCreate(self, NodeData):
122 """ Creates a node. Returns updated NodeData (without timestamp). """
123 return self._do("create", "node", NodeData)
125 def NodeUpdate(self, NodeData):
126 """ Updates node with NodeData. Returns updated NodeData (without timestamp). """
127 return self._do("update", "node", NodeData)
129 def NodeDelete(self, NodeData):
130 """ Delete node with NodeData. Returns updated NodeData (without timestamp). """
131 return self._do("delete", "node", NodeData)
133 def NodeHistory(self, NodeId):
134 """ Returns dict(NodeVerrsion: NodeData). """
135 uri = "/api/0.6/node/"+str(NodeId)+"/history"
136 data = self._get(uri)
137 data = xml.dom.minidom.parseString(data)
138 result = {}
139 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("node"):
140 data = self._DomParseNode(data)
141 result[data[u"version"]] = data
142 return result
144 def NodeWays(self, NodeId):
145 """ Returns [WayData, ... ] containing node #NodeId. """
146 uri = "/api/0.6/node/%d/ways"%NodeId
147 data = self._get(uri)
148 data = xml.dom.minidom.parseString(data)
149 result = []
150 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("way"):
151 data = self._DomParseRelation(data)
152 result.append(data)
153 return result
155 def NodeRelations(self, NodeId):
156 """ Returns [RelationData, ... ] containing node #NodeId. """
157 uri = "/api/0.6/node/%d/relations"%NodeId
158 data = self._get(uri)
159 data = xml.dom.minidom.parseString(data)
160 result = []
161 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("relation"):
162 data = self._DomParseRelation(data)
163 result.append(data)
164 return result
166 def NodesGet(self, NodeIdList):
167 """ Returns dict(NodeId: NodeData) for each node in NodeIdList """
168 uri = "/api/0.6/nodes?nodes=" + ",".join([str(x) for x in NodeIdList])
169 data = self._get(uri)
170 data = xml.dom.minidom.parseString(data)
171 result = {}
172 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("node"):
173 data = self._DomParseNode(data)
174 result[data[u"id"]] = data
175 return result
177 #######################################################################
178 # Way #
179 #######################################################################
181 def WayGet(self, WayId, WayVersion = -1):
182 """ Returns WayData for way #WayId. """
183 uri = "/api/0.6/way/"+str(WayId)
184 if WayVersion <> -1: uri += "/"+str(WayVersion)
185 data = self._get(uri)
186 if not data: return data
187 data = xml.dom.minidom.parseString(data)
188 data = data.getElementsByTagName("osm")[0].getElementsByTagName("way")[0]
189 return self._DomParseWay(data)
191 def WayCreate(self, WayData):
192 """ Creates a way. Returns updated WayData (without timestamp). """
193 return self._do("create", "way", WayData)
195 def WayUpdate(self, WayData):
196 """ Updates way with WayData. Returns updated WayData (without timestamp). """
197 return self._do("update", "way", WayData)
199 def WayDelete(self, WayData):
200 """ Delete way with WayData. Returns updated WayData (without timestamp). """
201 return self._do("delete", "way", WayData)
203 def WayHistory(self, WayId):
204 """ Returns dict(WayVerrsion: WayData). """
205 uri = "/api/0.6/way/"+str(WayId)+"/history"
206 data = self._get(uri)
207 data = xml.dom.minidom.parseString(data)
208 result = {}
209 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("way"):
210 data = self._DomParseWay(data)
211 result[data[u"version"]] = data
212 return result
214 def WayRelations(self, WayId):
215 """ Returns [RelationData, ...] containing way #WayId. """
216 uri = "/api/0.6/way/%d/relations"%WayId
217 data = self._get(uri)
218 data = xml.dom.minidom.parseString(data)
219 result = []
220 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("relation"):
221 data = self._DomParseRelation(data)
222 result.append(data)
223 return result
225 def WayFull(self, WayId):
226 """ Return full data for way WayId as list of {type: node|way|relation, data: {}}. """
227 uri = "/api/0.6/way/"+str(WayId)+"/full"
228 data = self._get(uri)
229 return self.ParseOsm(data)
231 def WaysGet(self, WayIdList):
232 """ Returns dict(WayId: WayData) for each way in WayIdList """
233 uri = "/api/0.6/ways?ways=" + ",".join([str(x) for x in WayIdList])
234 data = self._get(uri)
235 data = xml.dom.minidom.parseString(data)
236 result = {}
237 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("way"):
238 data = self._DomParseWay(data)
239 result[data[u"id"]] = data
240 return result
242 #######################################################################
243 # Relation #
244 #######################################################################
246 def RelationGet(self, RelationId, RelationVersion = -1):
247 """ Returns RelationData for relation #RelationId. """
248 uri = "/api/0.6/relation/"+str(RelationId)
249 if RelationVersion <> -1: uri += "/"+str(RelationVersion)
250 data = self._get(uri)
251 if not data: return data
252 data = xml.dom.minidom.parseString(data)
253 data = data.getElementsByTagName("osm")[0].getElementsByTagName("relation")[0]
254 return self._DomParseRelation(data)
256 def RelationCreate(self, RelationData):
257 """ Creates a relation. Returns updated RelationData (without timestamp). """
258 return self._do("create", "relation", RelationData)
260 def RelationUpdate(self, RelationData):
261 """ Updates relation with RelationData. Returns updated RelationData (without timestamp). """
262 return self._do("update", "relation", RelationData)
264 def RelationDelete(self, RelationData):
265 """ Delete relation with RelationData. Returns updated RelationData (without timestamp). """
266 return self._do("delete", "relation", RelationData)
268 def RelationHistory(self, RelationId):
269 """ Returns dict(RelationVerrsion: RelationData). """
270 uri = "/api/0.6/relation/"+str(RelationId)+"/history"
271 data = self._get(uri)
272 data = xml.dom.minidom.parseString(data)
273 result = {}
274 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("relation"):
275 data = self._DomParseRelation(data)
276 result[data[u"version"]] = data
277 return result
279 def RelationRelations(self, RelationId):
280 """ Returns list of RelationData containing relation #RelationId. """
281 uri = "/api/0.6/relation/%d/relations"%RelationId
282 data = self._get(uri)
283 data = xml.dom.minidom.parseString(data)
284 result = []
285 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("relation"):
286 data = self._DomParseRelation(data)
287 result.append(data)
288 return result
290 def RelationFull(self, RelationId):
291 """ Return full data for way WayId as list of {type: node|way|relation, data: {}}. """
292 uri = "/api/0.6/relation/"+str(RelationId)+"/full"
293 data = self._get(uri)
294 return self.ParseOsm(data)
296 def RelationsGet(self, RelationIdList):
297 """ Returns dict(RelationId: RelationData) for each relation in RelationIdList """
298 uri = "/api/0.6/relations?relations=" + ",".join([str(x) for x in RelationIdList])
299 data = self._get(uri)
300 data = xml.dom.minidom.parseString(data)
301 result = {}
302 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("relation"):
303 data = self._DomParseRelation(data)
304 result[data[u"id"]] = data
305 return result
307 #######################################################################
308 # Changeset #
309 #######################################################################
311 def ChangesetGet(self, ChangesetId):
312 """ Returns ChangesetData for changeset #ChangesetId. """
313 data = self._get("/api/0.6/changeset/"+str(ChangesetId))
314 data = xml.dom.minidom.parseString(data)
315 data = data.getElementsByTagName("osm")[0].getElementsByTagName("changeset")[0]
316 return self._DomParseChangeset(data)
318 def ChangesetUpdate(self, ChangesetTags = {}):
319 """ Updates current changeset with ChangesetTags. """
320 if self._CurrentChangesetId == -1:
321 raise Exception, "No changeset currently opened"
322 if u"created_by" not in ChangesetTags:
323 ChangesetTags[u"created_by"] = self._created_by
324 result = self._put("/api/0.6/changeset/"+str(self._CurrentChangesetId), self._XmlBuild("changeset", {u"tag": ChangesetTags}))
325 return self._CurrentChangesetId
327 def ChangesetCreate(self, ChangesetTags = {}):
328 """ Opens a changeset. Returns #ChangesetId. """
329 if self._CurrentChangesetId:
330 raise Exception, "Changeset alreadey opened"
331 if u"created_by" not in ChangesetTags:
332 ChangesetTags[u"created_by"] = self._created_by
333 result = self._put("/api/0.6/changeset/create", self._XmlBuild("changeset", {u"tag": ChangesetTags}))
334 self._CurrentChangesetId = int(result)
335 return self._CurrentChangesetId
337 def ChangesetClose(self):
338 """ Closes current changeset. Returns #ChangesetId. """
339 if not self._CurrentChangesetId:
340 raise Exception, "No changeset currently opened"
341 result = self._put("/api/0.6/changeset/"+str(self._CurrentChangesetId)+"/close", u"")
342 CurrentChangesetId = self._CurrentChangesetId
343 self._CurrentChangesetId = 0
344 return CurrentChangesetId
346 def ChangesetUpload(self):
347 raise NotImplemented
349 def ChangesetDownload(self, ChangesetId):
350 """ Download data from a changeset. Returns list of dict {type: node|way|relation, action: create|delete|modify, data: {}}. """
351 uri = "/api/0.6/changeset/"+str(ChangesetId)+"/download"
352 data = self._get(uri)
353 return self.ParseOsc(data)
355 def ChangesetsGet(self, min_lon=None, min_lat=None, max_lon=None, max_lat=None, userid=None, closed_after=None, created_before=None, only_open=False, only_closed=False):
356 """ Returns dict(ChangsetId: ChangesetData) matching all criteria. """
357 raise NotImplemented
359 #######################################################################
360 # Other #
361 #######################################################################
363 def Map(self, min_lon, min_lat, max_lon, max_lat):
364 """ Download data in bounding box. Returns list of dict {type: node|way|relation, data: {}}. """
365 uri = "/api/0.6/map?bbox=%f,%f,%f,%f"%(min_lon, min_lat, max_lon, max_lat)
366 data = self._get(uri)
367 return self.ParseOsm(data)
369 #######################################################################
370 # Data parser #
371 #######################################################################
373 def ParseOsm(self, data):
374 """ Parse osm data. Returns list of dict {type: node|way|relation, data: {}}. """
375 data = xml.dom.minidom.parseString(data)
376 data = data.getElementsByTagName("osm")[0]
377 result = []
378 for elem in data.childNodes:
379 if elem.nodeName == u"node":
380 result.append({u"type": elem.nodeName, u"data": self._DomParseNode(elem)})
381 elif elem.nodeName == u"way":
382 result.append({u"type": elem.nodeName, u"data": self._DomParseWay(elem)})
383 elif elem.nodeName == u"relation":
384 result.append({u"type": elem.nodeName, u"data": self._DomParseRelation(elem)})
385 return result
387 def ParseOsc(self, data):
388 """ Parse osc data. Returns list of dict {type: node|way|relation, action: create|delete|modify, data: {}}. """
389 data = xml.dom.minidom.parseString(data)
390 data = data.getElementsByTagName("osmChange")[0]
391 result = []
392 for action in data.childNodes:
393 if action.nodeName == u"#text": continue
394 for elem in action.childNodes:
395 if elem.nodeName == u"node":
396 result.append({u"action":action.nodeName, u"type": elem.nodeName, u"data": self._DomParseNode(elem)})
397 elif elem.nodeName == u"way":
398 result.append({u"action":action.nodeName, u"type": elem.nodeName, u"data": self._DomParseWay(elem)})
399 elif elem.nodeName == u"relation":
400 result.append({u"action":action.nodeName, u"type": elem.nodeName, u"data": self._DomParseRelation(elem)})
401 return result
403 #######################################################################
404 # Internal http function #
405 #######################################################################
407 def _do(self, action, OsmType, OsmData):
409 if u"timestamp" in OsmData:
410 OsmData.pop(u"timestamp")
412 if not self._CurrentChangesetId:
413 raise Exception, "You need to open a changeset before uploading data"
414 OsmData[u"changeset"] = self._CurrentChangesetId
415 if action == "create":
416 if OsmData.get(u"id", -1) > 0:
417 raise Exception, "This "+OsmType+" already exists"
418 result = self._put("/api/0.6/"+OsmType+"/create", self._XmlBuild(OsmType, OsmData))
419 OsmData[u"id"] = int(result.strip())
420 OsmData[u"version"] = 1
421 return OsmData
422 elif action == "update":
423 result = self._put("/api/0.6/"+OsmType+"/"+str(OsmData[u"id"]), self._XmlBuild(OsmType, OsmData))
424 OsmData[u"version"] = int(result.strip())
425 return OsmData
426 elif action =="delete":
427 result = self._delete("/api/0.6/"+OsmType+"/"+str(OsmData[u"id"]), self._XmlBuild(OsmType, OsmData))
428 OsmData[u"version"] = int(result.strip())
429 OsmData[u"visible"] = False
430 return OsmData
432 def _http_request(self, cmd, path, auth, send):
433 self._conn.putrequest(cmd, path)
434 self._conn.putheader('User-Agent', self._created_by)
435 if auth:
436 self._conn.putheader('Authorization', 'Basic ' + base64.encodestring(self._username + ':' + self._password).strip())
437 if send:
438 self._conn.putheader('Content-Length', len(send))
439 self._conn.endheaders()
440 if send:
441 self._conn.send(send)
442 response = self._conn.getresponse()
443 if response.status <> 200:
444 response.read()
445 if response.status == 410:
446 return None
447 raise Exception, "API returns unexpected status code "+str(response.status)+" ("+response.reason+")"
448 return response.read()
450 def _http(self, cmd, path, auth, send):
451 i = 0
452 while True:
453 i += 1
454 try:
455 return self._http_request(cmd, path, auth, send)
456 except:
457 if i == 5: raise
458 if i <> 1: time.sleep(2)
459 self._conn = httplib.HTTPConnection(self._api, 80)
461 def _get(self, path):
462 return self._http('GET', path, False, None)
464 def _put(self, path, data):
465 return self._http('PUT', path, True, data)
467 def _delete(self, path, data):
468 return self._http('DELETE', path, True, data)
470 #######################################################################
471 # Internal dom function #
472 #######################################################################
474 def _DomGetAttributes(self, DomElement):
475 """ Returns a formated dictionnary of attributes of a DomElement. """
476 result = {}
477 for k, v in DomElement.attributes.items():
478 if k == u"uid" : v = int(v)
479 elif k == u"changeset" : v = int(v)
480 elif k == u"version" : v = int(v)
481 elif k == u"id" : v = int(v)
482 elif k == u"lat" : v = float(v)
483 elif k == u"lon" : v = float(v)
484 elif k == u"open" : v = v=="true"
485 elif k == u"visible" : v = v=="true"
486 elif k == u"ref" : v = int(v)
487 result[k] = v
488 return result
490 def _DomGetTag(self, DomElement):
491 """ Returns the dictionnary of tags of a DomElement. """
492 result = {}
493 for t in DomElement.getElementsByTagName("tag"):
494 k = t.attributes["k"].value
495 v = t.attributes["v"].value
496 result[k] = v
497 return result
499 def _DomGetNd(self, DomElement):
500 """ Returns the list of nodes of a DomElement. """
501 result = []
502 for t in DomElement.getElementsByTagName("nd"):
503 result.append(int(int(t.attributes["ref"].value)))
504 return result
506 def _DomGetMember(self, DomElement):
507 """ Returns a list of relation members. """
508 result = []
509 for m in DomElement.getElementsByTagName("member"):
510 result.append(self._DomGetAttributes(m))
511 return result
513 def _DomParseNode(self, DomElement):
514 """ Returns NodeData for the node. """
515 result = self._DomGetAttributes(DomElement)
516 result[u"tag"] = self._DomGetTag(DomElement)
517 return result
519 def _DomParseWay(self, DomElement):
520 """ Returns WayData for the way. """
521 result = self._DomGetAttributes(DomElement)
522 result[u"tag"] = self._DomGetTag(DomElement)
523 result[u"nd"] = self._DomGetNd(DomElement)
524 return result
526 def _DomParseRelation(self, DomElement):
527 """ Returns RelationData for the relation. """
528 result = self._DomGetAttributes(DomElement)
529 result[u"tag"] = self._DomGetTag(DomElement)
530 result[u"member"] = self._DomGetMember(DomElement)
531 return result
533 def _DomParseChangeset(self, DomElement):
534 """ Returns ChangesetData for the changeset. """
535 result = self._DomGetAttributes(DomElement)
536 result[u"tag"] = self._DomGetTag(DomElement)
537 return result
539 #######################################################################
540 # Internal xml builder #
541 #######################################################################
543 def _XmlBuild(self, ElementType, ElementData):
545 xml = u""
546 xml += u"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
547 xml += u"<osm version=\"0.6\" generator=\"" + self._created_by + "\">\n"
549 # <element attr="val">
550 xml += u" <" + ElementType
551 if u"id" in ElementData:
552 xml += u" id=\"" + str(ElementData[u"id"]) + u"\""
553 if u"lat" in ElementData:
554 xml += u" lat=\"" + str(ElementData[u"lat"]) + u"\""
555 if u"lon" in ElementData:
556 xml += u" lon=\"" + str(ElementData[u"lon"]) + u"\""
557 if u"version" in ElementData:
558 xml += u" version=\"" + str(ElementData[u"version"]) + u"\""
559 xml += u" visible=\"" + str(ElementData.get(u"visible", True)).lower() + u"\""
560 if ElementType in [u"node", u"way", u"relation"]:
561 xml += u" changeset=\"" + str(self._CurrentChangesetId) + u"\""
562 xml += u">\n"
564 # <tag... />
565 for k, v in ElementData.get(u"tag", {}).items():
566 xml += u" <tag k=\""+self._XmlEncode(k)+u"\" v=\""+self._XmlEncode(v)+u"\"/>\n"
568 # <member... />
569 for member in ElementData.get(u"member", []):
570 xml += u" <member type=\""+member[u"type"]+"\" ref=\""+str(member[u"ref"])+u"\" role=\""+self._XmlEncode(member[u"role"])+"\"/>\n"
572 # <nd... />
573 for ref in ElementData.get(u"nd", []):
574 xml += u" <nd ref=\""+str(ref)+u"\"/>\n"
576 # </element>
577 xml += u" </" + ElementType + u">\n"
579 xml += u"</osm>\n"
581 return xml.encode("utf8")
583 def _XmlEncode(self, text):
584 return text.replace("&", "&amp;").replace("\"", "&quot;")
586 ## End of main class ##
587 ###########################################################################