use the same algo as Nini when deciding between village or hamlet
[osm-ro-tools.git] / OsmApi.py
blob6d613d9a6122cbc526f0077fb06fe658f6585b2f
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.2 2009-07-13 can identify applications built on top of the lib ##
28 ## 0.2.1 2009-05-05 some changes in constructor -- chove@crans.org ##
29 ## 0.2 2009-05-01 initial import ##
30 ###########################################################################
32 __version__ = '0.2.2'
34 import httplib, base64, xml.dom.minidom
36 ###########################################################################
37 ## Main class ##
39 class OsmApi:
41 def __init__(self, username = None, password = None, passwordfile = None, appid = "", created_by = "PythonOsmApi/"+__version__, api = "www.openstreetmap.org"):
43 # Get username
44 if username:
45 self._username = username
46 elif passwordfile:
47 self._username = open(passwordfile).readline().split(":")[0].strip()
49 # Get password
50 if password:
51 self._password = password
52 elif passwordfile:
53 for l in open(passwordfile).readlines():
54 l = l.strip().split(":")
55 if l[0] == self._username:
56 self._password = l[1]
58 # Get API
59 self._api = api
61 # Get created_by
62 if appid == "":
63 self._created_by = created_by
64 else:
65 self._created_by = appid + " (" + created_by + ")"
67 # Initialisation
68 self._CurrentChangesetId = -1
70 #######################################################################
71 # Capabilities #
72 #######################################################################
74 def Capabilities(self):
75 raise NotImplemented
77 #######################################################################
78 # Node #
79 #######################################################################
81 def NodeGet(self, NodeId, NodeVersion = -1):
82 """ Returns NodeData for node #NodeId. """
83 uri = "/api/0.6/node/"+str(NodeId)
84 if NodeVersion <> -1: uri += "/"+str(NodeVersion)
85 data = self._get(uri)
86 data = xml.dom.minidom.parseString(data.encode("utf-8"))
87 data = data.getElementsByTagName("osm")[0].getElementsByTagName("node")[0]
88 return self._DomParseNode(data)
90 def NodeUpdate(self, NodeData):
91 """ Updates node with NodeData. Returns updated NodeData (without timestamp). """
92 if self._CurrentChangesetId == -1:
93 raise Exception, "No changeset currently opened"
94 NodeData[u"changeset"] = self._CurrentChangesetId
95 result = self._put("/api/0.6/node/"+str(NodeData[u"id"]), self._XmlBuild("node", NodeData))
96 NodeData[u"version"] = int(result.strip())
97 if u"timestamp" in NodeData: NodeData.pop(u"timestamp")
98 return NodeData
100 def NodeDelete(self, NodeData):
101 """ Delete 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._delete("/api/0.6/node/"+str(NodeData[u"id"]), self._XmlBuild("node", NodeData))
106 NodeData[u"version"] = int(result.strip())
107 NodeData[u"visible"] = False
108 if u"timestamp" in NodeData: NodeData.pop(u"timestamp")
109 return NodeData
111 def NodeCreate(self, NodeData):
112 """ Creates a node. Returns updated NodeData (without timestamp). """
113 if self._CurrentChangesetId == -1:
114 raise Exception, "No changeset currently opened"
115 if NodeData.get(u"id", -1) > 0:
116 raise Exception, "This node already exists"
117 NodeData[u"changeset"] = self._CurrentChangesetId
118 result = self._put("/api/0.6/node/create", self._XmlBuild("node", NodeData))
119 NodeData[u"id"] = int(result.strip())
120 NodeData[u"version"] = 1
121 if u"timestamp" in NodeData: NodeData.pop(u"timestamp")
122 return NodeData
124 def NodeHistory(self, NodeId):
125 """ Returns dict(NodeVerrsion: NodeData). """
126 uri = "/api/0.6/node/"+str(NodeId)+"/history"
127 data = self._get(uri)
128 data = xml.dom.minidom.parseString(data.encode("utf-8"))
129 result = {}
130 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("node"):
131 data = self._DomParseNode(data)
132 result[data[u"version"]] = data
133 return result
135 def NodeWays(self, NodeId):
136 """ Returns [WayData, ... ] containing node #NodeId. """
137 # GET node/#/ways TODO
138 raise NotImplemented
140 def NodeRelations(self, NodeId):
141 """ Returns [RelationData, ... ] containing node #NodeId. """
142 # GET node/#/relations TODO
143 raise NotImplemented
145 def NodesGet(self, NodeIdList):
146 """ Will not be implemented. """
147 raise NotImplemented
149 #######################################################################
150 # Way #
151 #######################################################################
153 def WayGet(self, WayId, WayVersion = -1):
154 """ Returns WayData for way #WayId. """
155 uri = "/api/0.6/way/"+str(WayId)
156 if WayVersion <> -1: uri += "/"+str(WayVersion)
157 data = self._get(uri)
158 data = xml.dom.minidom.parseString(data.encode("utf-8"))
159 data = data.getElementsByTagName("osm")[0].getElementsByTagName("way")[0]
160 return self._DomParseWay(data)
162 def WayUpdate(self, WayData):
163 """ Updates way with WayData. Returns updated WayData (without timestamp). """
164 if self._CurrentChangesetId == -1:
165 raise Exception, "No changeset currently opened"
166 WayData[u"changeset"] = self._CurrentChangesetId
167 result = self._put("/api/0.6/way/"+str(WayData[u"id"]), self._XmlBuild("way", WayData))
168 WayData[u"version"] = int(result.strip())
169 if u"timestamp" in WayData: WayData.pop(u"timestamp")
170 return WayData
172 def WayDelete(self, WayData):
173 """ Delete way with WayData. Returns updated WayData (without timestamp). """
174 if self._CurrentChangesetId == -1:
175 raise Exception, "No changeset currently opened"
176 WayData[u"changeset"] = self._CurrentChangesetId
177 result = self._delete("/api/0.6/way/"+str(WayData[u"id"]), self._XmlBuild("way", WayData))
178 WayData[u"version"] = int(result.strip())
179 WayData[u"visible"] = False
180 if u"timestamp" in WayData: WayData.pop(u"timestamp")
181 return WayData
183 def WayCreate(self, WayData):
184 """ Creates a way. Returns updated WayData (without timestamp). """
185 if self._CurrentChangesetId == -1:
186 raise Exception, "No changeset currently opened"
187 if NodeData.get(u"id", -1) > 0:
188 raise Exception, "This way already exists"
189 WayData[u"changeset"] = self._CurrentChangesetId
190 result = self._put("/api/0.6/way/create", self._XmlBuild("way", WayData))
191 WayData[u"id"] = int(result.strip())
192 WayData[u"version"] = 1
193 if u"timestamp" in WayData: WayData.pop(u"timestamp")
194 return WayData
196 def WayHistory(self, WayId):
197 """ Returns dict(WayVerrsion: WayData). """
198 uri = "/api/0.6/way/"+str(WayId)+"/history"
199 data = self._get(uri)
200 data = xml.dom.minidom.parseString(data.encode("utf-8"))
201 result = {}
202 for data in data.getElementsByTagName("osm")[0].getElementsByTagName("way"):
203 data = self._DomParseWay(data)
204 result[data[u"version"]] = data
205 return result
207 def WayRelations(self, WayId):
208 """ Returns [RelationData, ...] containing way #WayId. """
209 # GET way/#/relations
210 raise NotImplemented
212 def WayFull(self, WayId):
213 """ Will not be implemented. """
214 raise NotImplemented
216 def WaysGet(self, WayIdList):
217 """ Will not be implemented. """
218 raise NotImplemented
220 #######################################################################
221 # Relation #
222 #######################################################################
224 def RelationGet(self, RelationId, RelationVersion = -1):
225 """ Returns RelationData for relation #RelationId. """
226 uri = "/api/0.6/relation/"+str(RelationId)
227 if RelationVersion <> -1: uri += "/"+str(RelationVersion)
228 data = self._get(uri)
229 #print type(data)
230 data = xml.dom.minidom.parseString(data.encode("utf-8"))
231 data = data.getElementsByTagName("osm")[0].getElementsByTagName("relation")[0]
232 return self._DomParseRelation(data)
234 def RelationUpdate(self, RelationData):
235 """ Updates relation with RelationData. Returns updated RelationData (without timestamp). """
236 if self._CurrentChangesetId == -1:
237 raise Exception, "No changeset currently opened"
238 RelationData[u"changeset"] = self._CurrentChangesetId
239 result = self._put("/api/0.6/relation/"+str(RelationData[u"id"]), self._XmlBuild("relation", RelationData))
240 RelationData[u"version"] = int(result.strip())
241 if u"timestamp" in RelationData: RelationData.pop(u"timestamp")
242 return RelationData
244 def RelationDelete(self, RelationData):
245 """ Delete relation with RelationData. Returns updated RelationData (without timestamp). """
246 if self._CurrentChangesetId == -1:
247 raise Exception, "No changeset currently opened"
248 RelationData[u"changeset"] = self._CurrentChangesetId
249 result = self._delete("/api/0.6/relation/"+str(RelationData[u"id"]), self._XmlBuild("relation", RelationData))
250 RelationData[u"version"] = int(result.strip())
251 RelationData[u"visible"] = False
252 if u"timestamp" in RelationData: RelationData.pop(u"timestamp")
253 return RelationData
255 def RelationCreate(self, RelationData):
256 """ Creates a relation. Returns updated RelationData (without timestamp). """
257 if self._CurrentChangesetId == -1:
258 raise Exception, "No changeset currently opened"
259 if NodeData.get(u"id", -1) > 0:
260 raise Exception, "This relation already exists"
261 RelationData[u"changeset"] = self._CurrentChangesetId
262 result = self._put("/api/0.6/relation/create", self._XmlBuild("relation", RelationData))
263 RelationData[u"id"] = int(result.strip())
264 RelationData[u"version"] = 1
265 if u"timestamp" in RelationData: RelationData.pop(u"timestamp")
266 return 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.encode("utf-8"))
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 # GET relation/#/relations TODO
282 raise NotImplemented
284 def RelationFull(self, RelationId):
285 """ Will not be implemented. """
286 raise NotImplemented
288 def RelationsGet(self, RelationIdList):
289 """ Will not be implemented. """
290 raise NotImplemented
292 #######################################################################
293 # Changeset #
294 #######################################################################
296 def ChangesetGet(self, ChangesetId):
297 """ Returns ChangesetData for changeset #ChangesetId. """
298 data = self._get("/api/0.6/changeset/"+str(ChangesetId))
299 data = xml.dom.minidom.parseString(data.encode("utf-8"))
300 data = data.getElementsByTagName("osm")[0].getElementsByTagName("changeset")[0]
301 return self._DomParseChangeset(data)
303 def ChangesetUpdate(self, ChangesetTags = {}):
304 """ Updates current changeset with ChangesetTags. """
305 if self._CurrentChangesetId == -1:
306 raise Exception, "No changeset currently opened"
307 if u"created_by" not in ChangesetTags:
308 ChangesetTags[u"created_by"] = self._created_by
309 result = self._put("/api/0.6/changeset/"+str(self._CurrentChangesetId), self._XmlBuild("changeset", {u"tag": ChangesetTags}))
310 return self._CurrentChangesetId
312 def ChangesetCreate(self, ChangesetTags = {}):
313 """ Opens a changeset. Returns #ChangesetId. """
314 if self._CurrentChangesetId <> -1:
315 raise Exception, "Changeset alreadey opened"
316 if u"created_by" not in ChangesetTags:
317 ChangesetTags[u"created_by"] = self._created_by
318 result = self._put("/api/0.6/changeset/create", self._XmlBuild("changeset", {u"tag": ChangesetTags}))
319 self._CurrentChangesetId = int(result)
320 self._CurrentChangesetTags = ChangesetTags
321 self._CurrentChangesetCpt = 0
322 return self._CurrentChangesetId
324 def ChangesetClose(self):
325 """ Closes current changeset. Returns #ChangesetId. """
326 if self._CurrentChangesetId == -1:
327 raise Exception, "No changeset currently opened"
328 result = self._put("/api/0.6/changeset/"+str(self._CurrentChangesetId)+"/close", u"")
329 CurrentChangesetId = self._CurrentChangesetId
330 self._CurrentChangesetId = -1
331 return CurrentChangesetId
333 def ChangesetUpload(self):
334 raise NotImplemented
336 def ChangesetDownload(self, ChangesetId):
337 """ Download data from a changeset. Returns list of dict {type: node|way|relation, action: create|delete|modify, data: {}}. """
338 uri = "/api/0.6/changeset/"+str(ChangesetId)+"/download"
339 data = self._get(uri)
340 data = xml.dom.minidom.parseString(data.encode("utf-8"))
341 data = data.getElementsByTagName("osmChange")[0]
342 result = []
343 for action in data.childNodes:
344 if action.nodeName == u"#text": continue
345 for elem in action.childNodes:
346 if elem.nodeName == u"node":
347 result.append({u"action":action.nodeName, u"type": elem.nodeName, u"data": self._DomParseNode(elem)})
348 elif elem.nodeName == u"way":
349 result.append({u"action":action.nodeName, u"type": elem.nodeName, u"data": self._DomParseWay(elem)})
350 elif elem.nodeName == u"relation":
351 result.append({u"action":action.nodeName, u"type": elem.nodeName, u"data": self._DomParseRelation(elem)})
352 return result
354 def ChangesetsGet(self):
355 raise NotImplemented
357 #######################################################################
358 # Other #
359 #######################################################################
361 def Map(self):
362 raise NotImplemented
364 def Trackpoints(self):
365 raise NotImplemented
367 def Changes(self):
368 raise NotImplemented
370 #######################################################################
371 # Internal http function #
372 #######################################################################
374 def _http(self, cmd, path, auth, send):
375 h = httplib.HTTPConnection(self._api, 80)
376 h.putrequest(cmd, path)
377 h.putheader('User-Agent', self._created_by)
378 if auth:
379 h.putheader('Authorization', 'Basic ' + base64.encodestring(self._username + ':' + self._password).strip())
380 if send:
381 send = send.encode("utf-8")
382 h.putheader('Content-Length', len(send))
383 h.endheaders()
384 if send:
385 h.send(send)
386 response = h.getresponse()
387 if response.status <> 200:
388 raise Exception, "API returns unexpected status code "+str(response.status)+" ("+response.reason+")"
389 return response.read().decode("utf-8")
391 def _get(self, path):
392 return self._http('GET', path, False, None)
394 def _put(self, path, data):
395 return self._http('PUT', path, True, data)
397 def _delete(self, path, data):
398 return self._http('DELETE', path, True, data)
400 #######################################################################
401 # Internal dom function #
402 #######################################################################
404 def _DomGetAttributes(self, DomElement):
405 """ Returns a formated dictionnary of attributes of a DomElement. """
406 result = {}
407 for k, v in DomElement.attributes.items():
408 k = k #.decode("utf8")
409 v = v #.decode("utf8")
410 if k == u"uid" : v = int(v)
411 elif k == u"changeset" : v = int(v)
412 elif k == u"version" : v = int(v)
413 elif k == u"id" : v = int(v)
414 elif k == u"lat" : v = float(v)
415 elif k == u"lon" : v = float(v)
416 elif k == u"open" : v = v=="true"
417 elif k == u"visible" : v = v=="true"
418 elif k == u"ref" : v = int(v)
419 result[k] = v
420 return result
422 def _DomGetTag(self, DomElement):
423 """ Returns the dictionnary of tags of a DomElement. """
424 result = {}
425 for t in DomElement.getElementsByTagName("tag"):
426 k = t.attributes["k"].value #.decode("utf8")
427 v = t.attributes["v"].value #.decode("utf8")
428 result[k] = v
429 return result
431 def _DomGetNd(self, DomElement):
432 """ Returns the list of nodes of a DomElement. """
433 result = []
434 for t in DomElement.getElementsByTagName("nd"):
435 result.append(int(int(t.attributes["ref"].value)))
436 return result
438 def _DomGetMember(self, DomElement):
439 """ Returns a list of relation members. """
440 result = []
441 for m in DomElement.getElementsByTagName("member"):
442 result.append(self._DomGetAttributes(m))
443 return result
445 def _DomParseNode(self, DomElement):
446 """ Returns NodeData for the node. """
447 result = self._DomGetAttributes(DomElement)
448 result[u"tag"] = self._DomGetTag(DomElement)
449 return result
451 def _DomParseWay(self, DomElement):
452 """ Returns WayData for the way. """
453 result = self._DomGetAttributes(DomElement)
454 result[u"tag"] = self._DomGetTag(DomElement)
455 result[u"nd"] = self._DomGetNd(DomElement)
456 return result
458 def _DomParseRelation(self, DomElement):
459 """ Returns RelationData for the relation. """
460 result = self._DomGetAttributes(DomElement)
461 result[u"tag"] = self._DomGetTag(DomElement)
462 result[u"member"] = self._DomGetMember(DomElement)
463 return result
465 def _DomParseChangeset(self, DomElement):
466 """ Returns ChangesetData for the changeset. """
467 result = self._DomGetAttributes(DomElement)
468 result[u"tag"] = self._DomGetTag(DomElement)
469 return result
471 #######################################################################
472 # Internal xml builder #
473 #######################################################################
475 def _XmlBuild(self, ElementType, ElementData):
477 xml = u""
478 xml += u"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
479 xml += u"<osm version=\"0.6\" generator=\"" + self._created_by + "\">\n"
481 # <element attr="val">
482 xml += u" <" + ElementType
483 if u"id" in ElementData:
484 xml += u" id=\"" + str(ElementData[u"id"]) + u"\""
485 if u"lat" in ElementData:
486 xml += u" lat=\"" + str(ElementData[u"lat"]) + u"\""
487 if u"lon" in ElementData:
488 xml += u" lon=\"" + str(ElementData[u"lon"]) + u"\""
489 if u"version" in ElementData:
490 xml += u" version=\"" + str(ElementData[u"version"]) + u"\""
491 xml += u" visible=\"" + str(ElementData.get(u"visible", True)).lower() + u"\""
492 if ElementType in [u"node", u"way", u"relation"]:
493 xml += u" changeset=\"" + str(self._CurrentChangesetId) + u"\""
494 xml += u">\n"
496 # <tag... />
497 for k, v in ElementData.get(u"tag", {}).items():
498 xml += u" <tag k=\""+self._XmlEncode(k)+u"\" v=\""+self._XmlEncode(v)+u"\"/>\n"
500 # <member... />
501 for member in ElementData.get(u"member", []):
502 xml += u" <member type=\""+member[u"type"]+"\" ref=\""+str(member[u"ref"])+u"\" role=\""+self._XmlEncode(member[u"role"])+"\"/>\n"
504 # <nd... />
505 for ref in ElementData.get(u"nd", []):
506 xml += u" <nd ref=\""+str(ref)+u"\"/>\n"
508 # </element>
509 xml += u" </" + ElementType + u">\n"
511 xml += u"</osm>\n"
513 return xml
515 def _XmlEncode(self, text):
516 return text.replace("&", "&amp;").replace("\"", "&quot;")
518 ## End of main class ##
519 ###########################################################################
522 ###########################################################################
523 ## Reverting tools : DO NOT USE SINCE IT'S INT DEV ##
525 def RevertAnalyse(self, ChangesetId):
526 """ Returns a string saying if changes are revertable or not. """
528 data = self.ChangesetDownload(ChangesetId)
529 result = u""
531 for elem in data:
533 if elem[u"type"] == u"node":
534 hist = self.NodeHistory(elem[u"data"][u"id"])
535 elem_latest = hist[sorted(hist.keys())[-1]]
536 elif elem[u"type"] == u"way":
537 hist = self.WayHistory(elem[u"data"][u"id"])
538 elem_latest = hist[sorted(hist.keys())[-1]]
539 elif elem[u"type"] == u"relation":
540 hist = self.RelationHistory(elem[u"data"][u"id"])
541 elem_latest = hist[sorted(hist.keys())[-1]]
543 result += elem[u"action"] + u" " * (10-len(elem[u"action"]))
544 result += elem[u"type"] + u" " * (10-len(elem[u"type"]))
545 result += str(elem[u"data"][u"id"]) + u" " * (10-len(str(elem[u"data"][u"id"])))
546 result += u" => "
547 if elem[u"data"][u"version"] == elem_latest[u"version"]:
548 result += u"revertable\n"
549 else:
550 result += u"not revertable\n"
552 return result.strip()
554 def RevertIfPossible(self, ChangesetId):
555 """ Revert all changes if possible. """
556 return
558 #z = OsmApi()
559 #print RevertAnalyse(z, 912290)
561 ## End of reverting tools ##
562 ###########################################################################