Added mark pilgram's open_anything function
[python-osm.git] / osm.py
blob69a4ea8b59e97accfe05daafe6676fa1e44d6b65
1 #! /usr/bin/python
2 import xml.sax, math, tempfile, urllib, urllib2, os
3 import StringIO
5 OSM_API_BASE_URL = "http://api.openstreetmaps.org/api/0.5"
7 class Property(object):
8 """
9 A metaclass to make it easy to add properties to objects
10 """
11 class __metaclass__(type):
12 def __init__(cls, name, bases, dct):
13 for fname in ['get', 'set', 'delete']:
14 if fname in dct:
15 setattr(cls, fname, staticmethod(dct[fname]))
16 def __get__(cls, obj, objtype=None):
17 if obj is None:
18 return cls
19 fget = getattr(cls, 'get')
20 return fget(obj)
21 def __set__(cls, obj, value):
22 fset = getattr(cls, 'set')
23 fset(obj, value)
24 def __delete__(cls, obj):
25 fdel = getattr(cls, 'delete')
26 fdel(obj)
29 class BBox(object):
30 __slots__ = ['left', 'right', 'top', 'bottom']
31 def __init__(self, *args, **kwargs):
32 if sorted(kwargs.keys()) == ['bottom', 'left', 'right', 'top']:
33 self.left = kwargs['left']
34 self.right = kwargs['right']
35 self.top = kwargs['top']
36 self.bottom = kwargs['bottom']
37 elif sorted(kwargs.keys()) == ['maxlat', 'maxlon', 'minlat', 'minlon']:
38 self.left = kwargs['minlat']
39 self.right = kwargs['maxlat']
40 self.bottom = kwargs['maxlon']
41 self.top = kwargs['minlon']
42 else:
43 raise TypeError("Insufficent arguments to BBox contsructor")
45 class minlat(Property):
46 def get(self):
47 return self.left
48 def set(self, value):
49 self.left = value
51 class maxlat(Property):
52 def get(self):
53 return self.right
54 def set(self, value):
55 self.right = value
57 class minlon(Property):
58 def get(self):
59 return self.top
60 def set(self, value):
61 self.top = value
63 class maxlon(Property):
64 def get(self):
65 return self.bottom
66 def set(self, value):
67 self.bottom = value
69 def __repr__(self):
70 return "BBox(left=%r, bottom=%r, right=%r, top=%r)" % (self.left, self.bottom, self.right, self.top)
72 def __in__(self, obj):
73 if instanceof(obj, Node):
74 return self.minlat < node.lat < self.maxlat and self.minlon < node.lon < self.maxlon
75 else:
76 raise TypeError("Object %r is not a node" % obj)
78 class Node(object):
79 __slots__ = ['id', 'lon', 'lat', 'tags']
81 def __init__(self, id=None, lon=None, lat=None, tags=None):
82 self.id = id
83 self.lon, self.lat = lon, lat
84 if tags:
85 self.tags = tags
86 else:
87 self.tags = {}
89 def __repr__(self):
90 return "Node(id=%r, lon=%r, lat=%r, tags=%r)" % (self.id, self.lon, self.lat, self.tags)
92 def distance(self, other):
93 """
94 Returns the distance between this point the other in metres
95 """
96 lat1=float(self.lat) * math.pi / 180
97 lat2=float(other.lat) * math.pi / 180
98 lon1=float(self.lon) * math.pi / 180
99 lon2=float(other.lon) * math.pi / 180
100 dist = math.atan(math.sqrt(math.pow(math.cos(lat2)*math.sin(abs(lon1-lon2)),2) + math.pow(math.cos(lat1)*math.sin(lat2) - math.sin(lat1)*math.cos(lat2)*math.cos(lon1-lon2),2)) / (math.sin(lat1)*math.sin(lat2) + math.cos(lat1)*math.cos(lat2)*math.cos(lon1-lon2)))
101 dist *= 6372795 # convert from radians to meters
102 return dist
104 class Way(object):
105 __slots__ = ['id', 'nodes', 'tags']
107 def __init__(self, id=None, nodes=None, tags=None):
108 self.id = id
109 if nodes:
110 self.nodes = nodes
111 else:
112 self.nodes = []
113 if tags:
114 self.tags = tags
115 else:
116 self.tags = {}
118 def __repr__(self):
119 return "Way(id=%r, nodes=%r, tags=%r)" % (self.id, self.nodes, self.tags)
121 def __len__(self):
123 Returns the length of the way in metres
125 if len(self.nodes) < 2:
126 return 0
127 return sum(self.nodes[i].distance(self.nodes[i+1]) for i in range(len(self.nodes)-1))
131 class NodePlaceHolder(object):
132 __slots__ = ['id']
134 def __init__(self, id):
135 self.id = id
137 def __repr__(self):
138 return "NodePlaceHolder(id=%r)" % (self.id)
140 class WayPlaceHolder(object):
141 __slots__ = ['id']
143 def __init__(self, id):
144 self.id = id
146 def __repr__(self):
147 return "WayPlaceHolder(id=%r)" % (self.id)
149 class Relation(object):
150 __slots__ = ['id', 'roles', 'tags']
152 def __init__(self, id):
153 self.id = id
154 self.roles = {}
155 self.tags = {}
157 def add(self, item, role=None):
159 Add the item to this relation with that role. If role is unspecified,
160 it's ""
162 if role == None:
163 role = ""
165 if role not in self.roles:
166 self.roles[role] = set()
167 self.roles[role].add(item)
171 class OSMXMLFile(object):
172 def __init__(self, datasource):
173 self.datasource = datasource
175 self.nodes = {}
176 self.ways = {}
177 self.relations = {}
178 self.invalid_ways = []
179 self.__parse()
182 def __parse(self):
183 """Parse the given XML file"""
185 if isinstance(self.datasource, basestring):
186 parser = xml.sax.parseString(self.datasource, OSMXMLFileParser(self))
187 else:
188 parser = xml.sax.parse(self.datasource, OSMXMLFileParser(self))
190 # now fix up all the refereneces
191 for index, way in self.ways.items():
192 try:
193 way.nodes = [self.nodes[node_pl.id] for node_pl in way.nodes]
194 except KeyError:
195 print "Way (id=%s) referes to a node that doesn't exist, skipping that way" % (index)
196 self.invalid_ways.append(way)
197 del self.ways[index]
198 continue
200 # convert them back to lists
201 self.nodes = self.nodes.values()
202 self.ways = self.ways.values()
205 class OSMXMLFileParser(xml.sax.ContentHandler):
206 def __init__(self, containing_obj):
207 self.containing_obj = containing_obj
208 self.curr_node = None
209 self.curr_way = None
210 self.curr_relation = None
212 def startElement(self, name, attrs):
213 #print "Start of node " + name
214 if name == 'node':
215 self.curr_node = Node(id=attrs['id'], lon=attrs['lon'], lat=attrs['lat'])
216 elif name == 'way':
217 #self.containing_obj.ways.append(Way())
218 self.curr_way = Way(id=attrs['id'])
219 elif name == 'relation':
220 self.curr_relation = Relation(id=attrs['id'])
221 elif name == 'tag':
222 #assert not self.curr_node and not self.curr_way, "curr_node (%r) and curr_way (%r) are both non-None" % (self.curr_node, self.curr_way)
223 if self.curr_node is not None:
224 self.curr_node.tags[attrs['k']] = attrs['v']
225 elif self.curr_way is not None:
226 self.curr_way.tags[attrs['k']] = attrs['v']
227 elif name == "nd":
228 assert self.curr_node is None, "curr_node (%r) is non-none" % (self.curr_node)
229 assert self.curr_way is not None, "curr_way is None"
230 self.curr_way.nodes.append(NodePlaceHolder(id=attrs['ref']))
231 elif name == "member":
232 #import pdb ; pdb.set_trace()
233 assert self.curr_relation is not None, "<member> tag and no relation"
234 if attrs['type'] == 'way':
235 self.curr_relation.add(WayPlaceHolder(id=attrs['ref']), role=attrs['role'])
236 elif attrs['type'] == 'node':
237 self.curr_relation.add(NodePlaceHolder(id=attrs['ref']), role=attrs['role'])
238 else:
239 assert False, "Unknown member type "+repr(attrs['type'])
240 elif name in ["osm", "bounds"]:
241 pass
242 else:
243 print "Unknown node: "+name
246 def endElement(self, name):
247 #print "End of node " + name
248 #assert not self.curr_node and not self.curr_way, "curr_node (%r) and curr_way (%r) are both non-None" % (self.curr_node, self.curr_way)
249 if name == "node":
250 self.containing_obj.nodes[self.curr_node.id] = self.curr_node
251 self.curr_node = None
252 elif name == "way":
253 self.containing_obj.ways[self.curr_way.id] = self.curr_way
254 self.curr_way = None
256 class GPSData(object):
258 Downloads data GPS track data from OpenStreetMap Server
260 def __init__(self, bbox, download=True):
261 self.bbox = bbox
262 self.tracks = []
263 if download:
264 self._download_from_api()
266 def _download_from_api(self):
267 url = "http://api.openstreetmap.org/api/0.5/trackpoints?bbox=%s,%s,%s,%s&page=%%d" % (
268 self.bbox.left, self.bbox.bottom, self.bbox.right, self.bbox.top)
270 page = 0
271 point_last_time = None
273 while page == 0 or point_last_time == 5000:
274 tmpfile_fp, tmpfilename = tempfile.mkstemp(suffix=".gpx",
275 prefix="osm-gps_%s,%s,%s,%s_%d_" % (self.bbox.left, self.bbox.bottom, self.bbox.right, self.bbox.top, page))
276 urllib.urlretrieve(url % page, filename=tmpfilename )
277 old_points_total = sum(len(way.nodes) for way in self.tracks)
278 self._parse_file(tmpfilename)
279 os.remove(tmpfilename)
280 point_last_time = sum(len(way.nodes) for way in self.tracks) - old_points_total
281 page += 1
284 def _parse_file(self, filename):
285 parser = xml.sax.make_parser()
286 parser.setContentHandler(GPXParser(self))
287 parser.parse(filename)
289 def save(self, filename):
290 fp = open(filename, 'w')
291 fp.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
292 fp.write("<gpx version=\"1.0\" creator=\"PyOSM\" xmlns=\"http://www.topografix.com/GPS/1/0/\">\n")
293 for track in self.tracks:
294 fp.write(" <trk>\n <trkseg>\n")
295 for node in track.nodes:
296 fp.write(' <trkpt lat="%s" lon="%s" />\n' % (node.lat, node.lon))
297 fp.write(" </trkseg>\n </trk>\n")
298 fp.write("</gpx>")
299 fp.close()
301 class GPXParser(xml.sax.ContentHandler):
303 Parses GPX files from the OSM GPS trackpoint downloader. Converts them to OSM format
305 def __init__(self, containing_obj):
306 self.tracks = []
307 self.__current_way = None
308 self.containing_obj = containing_obj
310 def startElement(self, name, attrs):
311 if name == "trkseg":
312 self.__current_way = Way()
313 elif name == "trkpt":
314 assert self.__current_way is not None, "Invalid GPX file, we've encountered a trkpt tag before a trkseg tag"
315 self.__current_way.nodes.append(Node(lat=attrs['lat'], lon=attrs['lon']))
317 def endElement(self, name):
318 if name == 'trkseg':
319 self.containing_obj.tracks.append(self.__current_way)
320 self.__current_way = None
322 class OSMServer(object):
323 def __init__(self, api_root):
324 self.api_root = api_root
326 def _get_data(self, subpath):
327 if subpath[0] != '/' and self.api_root[-1] != '/':
328 # print a warning?
329 pass
330 return urllib2.urlopen(self.api_root + subpath).read()
333 def node(self, node_id):
334 osm_data = OSMXMLFile(self._get_data("node/%s" % node_id))
336 if len(osm_data.nodes) == 0:
337 # no nodes found
338 return None
339 elif len(osm_data.nodes) == 1:
340 return osm_data.nodes[0]
342 def way(self, way_id):
343 osm_data = OSMXMLFile(self._get_data("way/%s" % way_id))
345 if len(osm_data.ways) == 0:
346 # no ways found
347 return None
348 elif len(osm_data.ways) == 1:
349 return osm_data.ways[0]
351 def relation(self, relation_id):
352 osm_data = OSMXMLFile(self._get_data("relation/%s" % relation_id))
354 if len(osm_data.relations) == 0:
355 # no relations found
356 return None
357 elif len(osm_data.relations) == 1:
358 return osm_data.relations[0]
361 # Mark Pilgram's excellent openAnything
362 def open_anything(source):
363 # try to open with urllib (if source is http, ftp, or file URL)
364 try:
365 return urllib.urlopen(source)
366 except (IOError, OSError):
367 pass
369 # try to open with native open function (if source is pathname)
370 try:
371 return open(source)
372 except (IOError, OSError):
373 pass
375 # treat source as string
376 return StringIO.StringIO(str(source))
380 osm_web = OSMServer("http://api.openstreetmap.org/api/0.5/")