imported OsmApi 0.2.5 from svn.openstreetmap.org
[osm-ro-tools.git] / OsmApi.py
blob95cc51fcf1314fc705c52e466453b89644daba73
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.5 2009-10-09 implements NodesGet, WaysGet, RelationsGet ##
28 ## ParseOsm, ParseOsc ##
29 ## 0.2.4 2009-10-06 clean-up ##
30 ## 0.2.3 2009-09-09 keep http connection alive for multiple request ##
31 ## (Node|Way|Relation)Get return None when object ##
32 ## have been deleted (raising error before) ##
33 ## 0.2.2 2009-07-13 can identify applications built on top of the lib ##
34 ## 0.2.1 2009-05-05 some changes in constructor -- chove@crans.org ##
35 ## 0.2 2009-05-01 initial import ##
36 ###########################################################################
38 __version__ = '0.2.5'
40 import httplib, base64, xml.dom.minidom, time
42 ###########################################################################
43 ## Main class ##
45 class OsmApi:
47 def __init__(self, username = None, password = None, passwordfile = None, appid = "", created_by = "PythonOsmApi/"+__version__, api = "www.openstreetmap.org"):
49 # Get username
50 if username:
51 self._username = username
52 elif passwordfile:
53 self._username = open(passwordfile).readline().split(":")[0].strip()
55 # Get password
56 if password:
57 self._password = password
58 elif passwordfile:
59 for l in open(passwordfile).readlines():
60 l = l.strip().split(":")
61 if l[0] == self._username:
62 self._password = l[1]
64 # Get API
65 self._api = api
67 # Get created_by
68 if appid == "":
69 self._created_by = created_by
70 else:
71 self._created_by = appid + " (" + created_by + ")"
73 # Initialisation
74 self._CurrentChangesetId = -1
76 # Http connection
77 self._conn = httplib.HTTPConnection(self._api, 80)
79 #######################################################################
80 # Capabilities #
81 #######################################################################
83 def Capabilities(self):
84 raise NotImplemented
86 #######################################################################
87 # Node #
88 #######################################################################
90 def NodeGet(self, NodeId, NodeVersion = -1):
91 """ Returns NodeData for node #NodeId. """
92 uri = "/api/0.6/node/"+str(NodeId)
93 if NodeVersion <> -1: uri += "/"+str(NodeVersion)
94 data = self._get(uri)
95 if not data: return data
96 data = xml.dom.minidom.parseString(data.encode("utf-8"))
97 data = data.getElementsByTagName("osm")[0].getElementsByTagName("node")[0]
98 return self._DomParseNode(data)
100 def NodeUpdate(self, NodeData):
101 """ Updates node with NodeData. Returns updated NodeData (without timestamp). """
102 if self._CurrentChangesetId == -1:
103 raise Exception, "No changeset currently opened"
104 NodeData[u"changeset"] = self._CurrentChangesetId
105 result = self._put("/api/0.6/node/"+str(NodeData[u"id"]), self._XmlBuild("node", NodeData))
106 NodeData[u"version"] = int(result.strip())
107 if u"timestamp" in NodeData: NodeData.pop(u"timestamp")
108 return NodeData
110 def NodeDelete(self, NodeData):
111 """ Delete node with NodeData. Returns updated NodeData (without timestamp). """
112 if self._CurrentChangesetId == -1:
113 raise Exception, "No changeset currently opened"
114 NodeData[u"changeset"] = self._CurrentChangesetId
115 result = self._delete("/api/0.6/node/"+str(NodeData[u"id"]), self._XmlBuild("node", NodeData))
116 NodeData[u"version"] = int(result.strip())
117 NodeData[u"visible"] = False
118 if u"timestamp" in NodeData: NodeData.pop(u"timestamp")
119 return NodeData
121 def NodeCreate(self, NodeData):
122 """ Creates a node. Returns updated NodeData (without timestamp). """
123 if self._CurrentChangesetId == -1:
124 raise Exception, "No changeset currently opened"
125 if NodeData.get(u"id", -1) > 0:
126 raise Exception, "This node already exists"
127 NodeData[u"changeset"] = self._CurrentChangesetId
128 result = self._put("/api/0.6/node/create", self._XmlBuild("node", NodeData))
129 NodeData[u"id"] = int(result.strip())
130 NodeData[u"version"] = 1
131 if u"timestamp" in NodeData: NodeData.pop(u"timestamp")
132 return NodeData
134 def NodeHistory(self, NodeId):
135 """ Returns dict(NodeVerrsion: NodeData). """
136 uri = "/api/0.6/node/"+str(NodeId)+"/history"
137 data = self._get(uri)
138 data = xml.dom.minidom.parseString(data.encode("utf-8"))
139 result = {}
140 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("node"):
141 data = self._DomParseNode(data)
142 result[data[u"version"]] = data
143 return result
145 def NodeWays(self, NodeId):
146 """ Returns [WayData, ... ] containing node #NodeId. """
147 # GET node/#/ways TODO
148 raise NotImplemented
150 def NodeRelations(self, NodeId):
151 """ Returns [RelationData, ... ] containing node #NodeId. """
152 # GET node/#/relations TODO
153 raise NotImplemented
155 def NodesGet(self, NodeIdList):
156 """ Returns dict(NodeId: NodeData) for each node in NodeIdList """
157 uri = "/api/0.6/nodes?nodes=" + ",".join([str(x) for x in NodeIdList])
158 data = self._get(uri)
159 data = xml.dom.minidom.parseString(data.encode("utf-8"))
160 result = {}
161 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("node"):
162 data = self._DomParseNode(data)
163 result[data[u"id"]] = data
164 return result
166 #######################################################################
167 # Way #
168 #######################################################################
170 def WayGet(self, WayId, WayVersion = -1):
171 """ Returns WayData for way #WayId. """
172 uri = "/api/0.6/way/"+str(WayId)
173 if WayVersion <> -1: uri += "/"+str(WayVersion)
174 data = self._get(uri)
175 if not data: return data
176 data = xml.dom.minidom.parseString(data.encode("utf-8"))
177 data = data.getElementsByTagName("osm")[0].getElementsByTagName("way")[0]
178 return self._DomParseWay(data)
180 def WayUpdate(self, WayData):
181 """ Updates way with WayData. Returns updated WayData (without timestamp). """
182 if self._CurrentChangesetId == -1:
183 raise Exception, "No changeset currently opened"
184 WayData[u"changeset"] = self._CurrentChangesetId
185 result = self._put("/api/0.6/way/"+str(WayData[u"id"]), self._XmlBuild("way", WayData))
186 WayData[u"version"] = int(result.strip())
187 if u"timestamp" in WayData: WayData.pop(u"timestamp")
188 return WayData
190 def WayDelete(self, WayData):
191 """ Delete way with WayData. Returns updated WayData (without timestamp). """
192 if self._CurrentChangesetId == -1:
193 raise Exception, "No changeset currently opened"
194 WayData[u"changeset"] = self._CurrentChangesetId
195 result = self._delete("/api/0.6/way/"+str(WayData[u"id"]), self._XmlBuild("way", WayData))
196 WayData[u"version"] = int(result.strip())
197 WayData[u"visible"] = False
198 if u"timestamp" in WayData: WayData.pop(u"timestamp")
199 return WayData
201 def WayCreate(self, WayData):
202 """ Creates a way. Returns updated WayData (without timestamp). """
203 if self._CurrentChangesetId == -1:
204 raise Exception, "No changeset currently opened"
205 if NodeData.get(u"id", -1) > 0:
206 raise Exception, "This way already exists"
207 WayData[u"changeset"] = self._CurrentChangesetId
208 result = self._put("/api/0.6/way/create", self._XmlBuild("way", WayData))
209 WayData[u"id"] = int(result.strip())
210 WayData[u"version"] = 1
211 if u"timestamp" in WayData: WayData.pop(u"timestamp")
212 return WayData
214 def WayHistory(self, WayId):
215 """ Returns dict(WayVerrsion: WayData). """
216 uri = "/api/0.6/way/"+str(WayId)+"/history"
217 data = self._get(uri)
218 data = xml.dom.minidom.parseString(data.encode("utf-8"))
219 result = {}
220 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("way"):
221 data = self._DomParseWay(data)
222 result[data[u"version"]] = data
223 return result
225 def WayRelations(self, WayId):
226 """ Returns [RelationData, ...] containing way #WayId. """
227 # GET way/#/relations
228 raise NotImplemented
230 def WayFull(self, WayId):
231 """ Will not be implemented. """
232 raise NotImplemented
234 def WaysGet(self, WayIdList):
235 """ Returns dict(WayId: WayData) for each way in WayIdList """
236 uri = "/api/0.6/ways?ways=" + ",".join([str(x) for x in WayIdList])
237 data = self._get(uri)
238 data = xml.dom.minidom.parseString(data.encode("utf-8"))
239 result = {}
240 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("way"):
241 data = self._DomParseWay(data)
242 result[data[u"id"]] = data
243 return result
245 #######################################################################
246 # Relation #
247 #######################################################################
249 def RelationGet(self, RelationId, RelationVersion = -1):
250 """ Returns RelationData for relation #RelationId. """
251 uri = "/api/0.6/relation/"+str(RelationId)
252 if RelationVersion <> -1: uri += "/"+str(RelationVersion)
253 data = self._get(uri)
254 if not data: return data
255 data = xml.dom.minidom.parseString(data.encode("utf-8"))
256 data = data.getElementsByTagName("osm")[0].getElementsByTagName("relation")[0]
257 return self._DomParseRelation(data)
259 def RelationUpdate(self, RelationData):
260 """ Updates relation with RelationData. Returns updated RelationData (without timestamp). """
261 if self._CurrentChangesetId == -1:
262 raise Exception, "No changeset currently opened"
263 RelationData[u"changeset"] = self._CurrentChangesetId
264 result = self._put("/api/0.6/relation/"+str(RelationData[u"id"]), self._XmlBuild("relation", RelationData))
265 RelationData[u"version"] = int(result.strip())
266 if u"timestamp" in RelationData: RelationData.pop(u"timestamp")
267 return RelationData
269 def RelationDelete(self, RelationData):
270 """ Delete relation with RelationData. Returns updated RelationData (without timestamp). """
271 if self._CurrentChangesetId == -1:
272 raise Exception, "No changeset currently opened"
273 RelationData[u"changeset"] = self._CurrentChangesetId
274 result = self._delete("/api/0.6/relation/"+str(RelationData[u"id"]), self._XmlBuild("relation", RelationData))
275 RelationData[u"version"] = int(result.strip())
276 RelationData[u"visible"] = False
277 if u"timestamp" in RelationData: RelationData.pop(u"timestamp")
278 return RelationData
280 def RelationCreate(self, RelationData):
281 """ Creates a relation. Returns updated RelationData (without timestamp). """
282 if self._CurrentChangesetId == -1:
283 raise Exception, "No changeset currently opened"
284 if NodeData.get(u"id", -1) > 0:
285 raise Exception, "This relation already exists"
286 RelationData[u"changeset"] = self._CurrentChangesetId
287 result = self._put("/api/0.6/relation/create", self._XmlBuild("relation", RelationData))
288 RelationData[u"id"] = int(result.strip())
289 RelationData[u"version"] = 1
290 if u"timestamp" in RelationData: RelationData.pop(u"timestamp")
291 return RelationData
293 def RelationHistory(self, RelationId):
294 """ Returns dict(RelationVerrsion: RelationData). """
295 uri = "/api/0.6/relation/"+str(RelationId)+"/history"
296 data = self._get(uri)
297 data = xml.dom.minidom.parseString(data.encode("utf-8"))
298 result = {}
299 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("relation"):
300 data = self._DomParseRelation(data)
301 result[data[u"version"]] = data
302 return result
304 def RelationRelations(self, RelationId):
305 """ Returns list of RelationData containing relation #RelationId. """
306 # GET relation/#/relations TODO
307 raise NotImplemented
309 def RelationFull(self, RelationId):
310 """ Will not be implemented. """
311 raise NotImplemented
313 def RelationsGet(self, RelationIdList):
314 """ Returns dict(RelationId: RelationData) for each relation in RelationIdList """
315 uri = "/api/0.6/relations?relations=" + ",".join([str(x) for x in RelationIdList])
316 data = self._get(uri)
317 data = xml.dom.minidom.parseString(data.encode("utf-8"))
318 result = {}
319 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("relation"):
320 data = self._DomParseRelation(data)
321 result[data[u"id"]] = data
322 return result
324 #######################################################################
325 # Changeset #
326 #######################################################################
328 def ChangesetGet(self, ChangesetId):
329 """ Returns ChangesetData for changeset #ChangesetId. """
330 data = self._get("/api/0.6/changeset/"+str(ChangesetId))
331 data = xml.dom.minidom.parseString(data.encode("utf-8"))
332 data = data.getElementsByTagName("osm")[0].getElementsByTagName("changeset")[0]
333 return self._DomParseChangeset(data)
335 def ChangesetUpdate(self, ChangesetTags = {}):
336 """ Updates current changeset with ChangesetTags. """
337 if self._CurrentChangesetId == -1:
338 raise Exception, "No changeset currently opened"
339 if u"created_by" not in ChangesetTags:
340 ChangesetTags[u"created_by"] = self._created_by
341 result = self._put("/api/0.6/changeset/"+str(self._CurrentChangesetId), self._XmlBuild("changeset", {u"tag": ChangesetTags}))
342 return self._CurrentChangesetId
344 def ChangesetCreate(self, ChangesetTags = {}):
345 """ Opens a changeset. Returns #ChangesetId. """
346 if self._CurrentChangesetId <> -1:
347 raise Exception, "Changeset alreadey opened"
348 if u"created_by" not in ChangesetTags:
349 ChangesetTags[u"created_by"] = self._created_by
350 result = self._put("/api/0.6/changeset/create", self._XmlBuild("changeset", {u"tag": ChangesetTags}))
351 self._CurrentChangesetId = int(result)
352 self._CurrentChangesetTags = ChangesetTags
353 self._CurrentChangesetCpt = 0
354 return self._CurrentChangesetId
356 def ChangesetClose(self):
357 """ Closes current changeset. Returns #ChangesetId. """
358 if self._CurrentChangesetId == -1:
359 raise Exception, "No changeset currently opened"
360 result = self._put("/api/0.6/changeset/"+str(self._CurrentChangesetId)+"/close", u"")
361 CurrentChangesetId = self._CurrentChangesetId
362 self._CurrentChangesetId = -1
363 return CurrentChangesetId
365 def ChangesetUpload(self):
366 raise NotImplemented
368 def ChangesetDownload(self, ChangesetId):
369 """ Download data from a changeset. Returns list of dict {type: node|way|relation, action: create|delete|modify, data: {}}. """
370 uri = "/api/0.6/changeset/"+str(ChangesetId)+"/download"
371 data = self._get(uri)
372 return self.ParseOsc(data.encode("utf-8"))
374 def ChangesetsGet(self):
375 raise NotImplemented
377 #######################################################################
378 # Other #
379 #######################################################################
381 def Map(self):
382 raise NotImplemented
384 def Trackpoints(self):
385 raise NotImplemented
387 def Changes(self):
388 raise NotImplemented
390 #######################################################################
391 # Data parser #
392 #######################################################################
394 def ParseOsm(self, data):
395 """ Parse osm data. Returns list of dict {type: node|way|relation, data: {}}. """
396 data = xml.dom.minidom.parseString(data)
397 data = data.getElementsByTagName("osm")[0]
398 result = []
399 for elem in data.childNodes:
400 if elem.nodeName == u"node":
401 result.append({u"type": elem.nodeName, u"data": self._DomParseNode(elem)})
402 elif elem.nodeName == u"way":
403 result.append({u"type": elem.nodeName, u"data": self._DomParseWay(elem)})
404 elif elem.nodeName == u"relation":
405 result.append({u"type": elem.nodeName, u"data": self._DomParseRelation(elem)})
406 return result
408 def ParseOsc(self, data):
409 """ Parse osc data. Returns list of dict {type: node|way|relation, action: create|delete|modify, data: {}}. """
410 data = xml.dom.minidom.parseString(data)
411 data = data.getElementsByTagName("osmChange")[0]
412 result = []
413 for action in data.childNodes:
414 if action.nodeName == u"#text": continue
415 for elem in action.childNodes:
416 if elem.nodeName == u"node":
417 result.append({u"action":action.nodeName, u"type": elem.nodeName, u"data": self._DomParseNode(elem)})
418 elif elem.nodeName == u"way":
419 result.append({u"action":action.nodeName, u"type": elem.nodeName, u"data": self._DomParseWay(elem)})
420 elif elem.nodeName == u"relation":
421 result.append({u"action":action.nodeName, u"type": elem.nodeName, u"data": self._DomParseRelation(elem)})
422 return result
424 #######################################################################
425 # Internal http function #
426 #######################################################################
428 def _http_request(self, cmd, path, auth, send):
429 self._conn.putrequest(cmd, path)
430 self._conn.putheader('User-Agent', self._created_by)
431 if auth:
432 self._conn.putheader('Authorization', 'Basic ' + base64.encodestring(self._username + ':' + self._password).strip())
433 if send:
434 send = send.encode("utf-8")
435 self._conn.putheader('Content-Length', len(send))
436 self._conn.endheaders()
437 if send:
438 self._conn.send(send)
439 response = self._conn.getresponse()
440 if response.status <> 200:
441 response.read()
442 if response.status == 410:
443 return None
444 raise Exception, "API returns unexpected status code "+str(response.status)+" ("+response.reason+")"
445 return response.read().decode("utf-8")
447 def _http(self, cmd, path, auth, send):
448 i = 0
449 while True:
450 i += 1
451 try:
452 return self._http_request(cmd, path, auth, send)
453 except:
454 if i == 5: raise
455 if i <> 1: time.sleep(2)
456 self._conn = httplib.HTTPConnection(self._api, 80)
458 def _get(self, path):
459 return self._http('GET', path, False, None)
461 def _put(self, path, data):
462 return self._http('PUT', path, True, data)
464 def _delete(self, path, data):
465 return self._http('DELETE', path, True, data)
467 #######################################################################
468 # Internal dom function #
469 #######################################################################
471 def _DomGetAttributes(self, DomElement):
472 """ Returns a formated dictionnary of attributes of a DomElement. """
473 result = {}
474 for k, v in DomElement.attributes.items():
475 k = k #.decode("utf8")
476 v = v #.decode("utf8")
477 if k == u"uid" : v = int(v)
478 elif k == u"changeset" : v = int(v)
479 elif k == u"version" : v = int(v)
480 elif k == u"id" : v = int(v)
481 elif k == u"lat" : v = float(v)
482 elif k == u"lon" : v = float(v)
483 elif k == u"open" : v = v=="true"
484 elif k == u"visible" : v = v=="true"
485 elif k == u"ref" : v = int(v)
486 result[k] = v
487 return result
489 def _DomGetTag(self, DomElement):
490 """ Returns the dictionnary of tags of a DomElement. """
491 result = {}
492 for t in DomElement.getElementsByTagName("tag"):
493 k = t.attributes["k"].value #.decode("utf8")
494 v = t.attributes["v"].value #.decode("utf8")
495 result[k] = v
496 return result
498 def _DomGetNd(self, DomElement):
499 """ Returns the list of nodes of a DomElement. """
500 result = []
501 for t in DomElement.getElementsByTagName("nd"):
502 result.append(int(int(t.attributes["ref"].value)))
503 return result
505 def _DomGetMember(self, DomElement):
506 """ Returns a list of relation members. """
507 result = []
508 for m in DomElement.getElementsByTagName("member"):
509 result.append(self._DomGetAttributes(m))
510 return result
512 def _DomParseNode(self, DomElement):
513 """ Returns NodeData for the node. """
514 result = self._DomGetAttributes(DomElement)
515 result[u"tag"] = self._DomGetTag(DomElement)
516 return result
518 def _DomParseWay(self, DomElement):
519 """ Returns WayData for the way. """
520 result = self._DomGetAttributes(DomElement)
521 result[u"tag"] = self._DomGetTag(DomElement)
522 result[u"nd"] = self._DomGetNd(DomElement)
523 return result
525 def _DomParseRelation(self, DomElement):
526 """ Returns RelationData for the relation. """
527 result = self._DomGetAttributes(DomElement)
528 result[u"tag"] = self._DomGetTag(DomElement)
529 result[u"member"] = self._DomGetMember(DomElement)
530 return result
532 def _DomParseChangeset(self, DomElement):
533 """ Returns ChangesetData for the changeset. """
534 result = self._DomGetAttributes(DomElement)
535 result[u"tag"] = self._DomGetTag(DomElement)
536 return result
538 #######################################################################
539 # Internal xml builder #
540 #######################################################################
542 def _XmlBuild(self, ElementType, ElementData):
544 xml = u""
545 xml += u"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
546 xml += u"<osm version=\"0.6\" generator=\"" + self._created_by + "\">\n"
548 # <element attr="val">
549 xml += u" <" + ElementType
550 if u"id" in ElementData:
551 xml += u" id=\"" + str(ElementData[u"id"]) + u"\""
552 if u"lat" in ElementData:
553 xml += u" lat=\"" + str(ElementData[u"lat"]) + u"\""
554 if u"lon" in ElementData:
555 xml += u" lon=\"" + str(ElementData[u"lon"]) + u"\""
556 if u"version" in ElementData:
557 xml += u" version=\"" + str(ElementData[u"version"]) + u"\""
558 xml += u" visible=\"" + str(ElementData.get(u"visible", True)).lower() + u"\""
559 if ElementType in [u"node", u"way", u"relation"]:
560 xml += u" changeset=\"" + str(self._CurrentChangesetId) + u"\""
561 xml += u">\n"
563 # <tag... />
564 for k, v in ElementData.get(u"tag", {}).items():
565 xml += u" <tag k=\""+self._XmlEncode(k)+u"\" v=\""+self._XmlEncode(v)+u"\"/>\n"
567 # <member... />
568 for member in ElementData.get(u"member", []):
569 xml += u" <member type=\""+member[u"type"]+"\" ref=\""+str(member[u"ref"])+u"\" role=\""+self._XmlEncode(member[u"role"])+"\"/>\n"
571 # <nd... />
572 for ref in ElementData.get(u"nd", []):
573 xml += u" <nd ref=\""+str(ref)+u"\"/>\n"
575 # </element>
576 xml += u" </" + ElementType + u">\n"
578 xml += u"</osm>\n"
580 return xml
582 def _XmlEncode(self, text):
583 return text.replace("&", "&amp;").replace("\"", "&quot;")
585 ## End of main class ##
586 ###########################################################################