This repository has been archived by the owner on May 7, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 23
/
osm.py
executable file
·424 lines (340 loc) · 13.6 KB
/
osm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
#! /usr/bin/python
import xml.sax, math, tempfile, urllib, urllib2, os, math
import StringIO
OSM_API_BASE_URL = "http://api.openstreetmaps.org/api/0.5"
class Property(object):
"""
A metaclass to make it easy to add properties to objects
"""
class __metaclass__(type):
def __init__(cls, name, bases, dct):
for fname in ['get', 'set', 'delete']:
if fname in dct:
setattr(cls, fname, staticmethod(dct[fname]))
def __get__(cls, obj, objtype=None):
if obj is None:
return cls
fget = getattr(cls, 'get')
return fget(obj)
def __set__(cls, obj, value):
fset = getattr(cls, 'set')
fset(obj, value)
def __delete__(cls, obj):
fdel = getattr(cls, 'delete')
fdel(obj)
class BBox(object):
__slots__ = ['left', 'right', 'top', 'bottom']
def __init__(self, *args, **kwargs):
if sorted(kwargs.keys()) == ['bottom', 'left', 'right', 'top']:
self.left = kwargs['left']
self.right = kwargs['right']
self.top = kwargs['top']
self.bottom = kwargs['bottom']
elif sorted(kwargs.keys()) == ['maxlat', 'maxlon', 'minlat', 'minlon']:
self.left = kwargs['minlat']
self.right = kwargs['maxlat']
self.bottom = kwargs['maxlon']
self.top = kwargs['minlon']
else:
raise TypeError("Insufficent arguments to BBox contsructor")
class minlat(Property):
def get(self):
return self.left
def set(self, value):
self.left = value
class maxlat(Property):
def get(self):
return self.right
def set(self, value):
self.right = value
class minlon(Property):
def get(self):
return self.top
def set(self, value):
self.top = value
class maxlon(Property):
def get(self):
return self.bottom
def set(self, value):
self.bottom = value
def __repr__(self):
return "BBox(left=%r, bottom=%r, right=%r, top=%r)" % (self.left, self.bottom, self.right, self.top)
def __in__(self, obj):
if instanceof(obj, Node):
return self.minlat < node.lat < self.maxlat and self.minlon < node.lon < self.maxlon
else:
raise TypeError("Object %r is not a node" % obj)
class Node(object):
__slots__ = ['id', 'lon', 'lat', 'tags']
def __init__(self, id=None, lon=None, lat=None, tags=None):
self.id = id
self.lon, self.lat = lon, lat
if tags:
self.tags = tags
else:
self.tags = {}
def __repr__(self):
return "Node(id=%r, lon=%r, lat=%r, tags=%r)" % (self.id, self.lon, self.lat, self.tags)
def distance(self, other):
"""
Returns the distance between this point the other in metres
"""
lat1=float(self.lat) * math.pi / 180
lat2=float(other.lat) * math.pi / 180
lon1=float(self.lon) * math.pi / 180
lon2=float(other.lon) * math.pi / 180
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)))
dist *= 6372795 # convert from radians to meters
return dist
class Way(object):
__slots__ = ['id', 'nodes', 'tags']
def __init__(self, id=None, nodes=None, tags=None):
self.id = id
if nodes:
self.nodes = nodes
else:
self.nodes = []
if tags:
self.tags = tags
else:
self.tags = {}
def __repr__(self):
return "Way(id=%r, nodes=%r, tags=%r)" % (self.id, self.nodes, self.tags)
def __len__(self):
"""
Returns the length of the way in metres
"""
if len(self.nodes) < 2:
return 0
return sum(self.nodes[i].distance(self.nodes[i+1]) for i in range(len(self.nodes)-1))
class NodePlaceHolder(object):
__slots__ = ['id']
def __init__(self, id):
self.id = id
def __repr__(self):
return "NodePlaceHolder(id=%r)" % (self.id)
class WayPlaceHolder(object):
__slots__ = ['id']
def __init__(self, id):
self.id = id
def __repr__(self):
return "WayPlaceHolder(id=%r)" % (self.id)
class RelationPlaceHolder(object):
__slots__ = ['id']
def __init__(self, id):
self.id = id
def __repr__(self):
return "RelationPlaceHolder(id=%r)" % (self.id)
class Relation(object):
__slots__ = ['id', 'roles', 'tags']
def __init__(self, id):
self.id = id
self.roles = {}
self.tags = {}
def add(self, item, role=None):
"""
Add the item to this relation with that role. If role is unspecified,
it's ""
"""
if role == None:
role = ""
if role not in self.roles:
self.roles[role] = set()
self.roles[role].add(item)
class OSMXMLFile(object):
def __init__(self, datasource, parser_filter=None):
self.datasource = datasource
self.parser_filter = parser_filter
self.nodes = {}
self.ways = {}
self.relations = {}
self.invalid_ways = []
self.__parse()
def __parse(self):
"""Parse the given XML file"""
if isinstance(self.datasource, basestring):
parser = xml.sax.parseString(self.datasource, OSMXMLFileParser(self, filter=self.parser_filter))
else:
parser = xml.sax.parse(self.datasource, OSMXMLFileParser(self, filter=self.parser_filter))
# now fix up all the refereneces
for index, way in self.ways.items():
try:
way.nodes = [self.nodes[node_pl.id] for node_pl in way.nodes]
except KeyError:
print "Way (id=%s) referes to a node that doesn't exist, skipping that way" % (index)
self.invalid_ways.append(way)
del self.ways[index]
continue
# convert them back to lists
self.nodes = self.nodes.values()
self.ways = self.ways.values()
class OSMXMLFileParser(xml.sax.ContentHandler):
def __init__(self, containing_obj, filter=None):
self.containing_obj = containing_obj
self.curr_node = None
self.curr_way = None
self.curr_relation = None
if filter:
self.filter = filter
else:
# We're not doin any filtering
self.filter = lambda item: item
def startElement(self, name, attrs):
#print "Start of node " + name
if name == 'node':
self.curr_node = Node(id=attrs['id'], lon=attrs['lon'], lat=attrs['lat'])
elif name == 'way':
#self.containing_obj.ways.append(Way())
self.curr_way = Way(id=attrs['id'])
elif name == 'relation':
self.curr_relation = Relation(id=attrs['id'])
elif name == 'tag':
#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)
if self.curr_node is not None:
self.curr_node.tags[attrs['k']] = attrs['v']
elif self.curr_way is not None:
self.curr_way.tags[attrs['k']] = attrs['v']
elif name == "nd":
assert self.curr_node is None, "curr_node (%r) is non-none" % (self.curr_node)
assert self.curr_way is not None, "curr_way is None"
self.curr_way.nodes.append(NodePlaceHolder(id=attrs['ref']))
elif name == "member":
#import pdb ; pdb.set_trace()
assert self.curr_relation is not None, "<member> tag and no relation"
if attrs['type'] == 'way':
self.curr_relation.add(WayPlaceHolder(id=attrs['ref']), role=attrs['role'])
elif attrs['type'] == 'node':
self.curr_relation.add(NodePlaceHolder(id=attrs['ref']), role=attrs['role'])
elif attrs['type'] == 'relation':
self.curr_relation.add(RelationPlaceHolder(id=attrs['ref']), role=attrs['role'])
else:
assert False, "Unknown member type "+repr(attrs['type'])
elif name in ["osm", "bounds"]:
pass
else:
print "Unknown node: "+name
def endElement(self, name):
#print "End of node " + name
#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)
if name == "node":
node = self.filter(self.curr_node)
if node is not None:
self.containing_obj.nodes[node.id] = node
self.curr_node = None
elif name == "way":
way = self.filter(self.curr_way)
if way is not None:
self.containing_obj.ways[way.id] = way
self.curr_way = None
class GPSData(object):
"""
Downloads data GPS track data from OpenStreetMap Server
"""
def __init__(self, bbox, download=True):
self.bbox = bbox
self.tracks = []
if download:
self._download_from_api()
def _download_from_api(self):
url = "http://api.openstreetmap.org/api/0.5/trackpoints?bbox=%s,%s,%s,%s&page=%%d" % (
self.bbox.left, self.bbox.bottom, self.bbox.right, self.bbox.top)
page = 0
point_last_time = None
while page == 0 or point_last_time == 5000:
tmpfile_fp, tmpfilename = tempfile.mkstemp(suffix=".gpx",
prefix="osm-gps_%s,%s,%s,%s_%d_" % (self.bbox.left, self.bbox.bottom, self.bbox.right, self.bbox.top, page))
urllib.urlretrieve(url % page, filename=tmpfilename )
old_points_total = sum(len(way.nodes) for way in self.tracks)
self._parse_file(tmpfilename)
os.remove(tmpfilename)
point_last_time = sum(len(way.nodes) for way in self.tracks) - old_points_total
page += 1
def _parse_file(self, filename):
parser = xml.sax.make_parser()
parser.setContentHandler(GPXParser(self))
parser.parse(filename)
def save(self, filename):
fp = open(filename, 'w')
fp.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
fp.write("<gpx version=\"1.0\" creator=\"PyOSM\" xmlns=\"http://www.topografix.com/GPS/1/0/\">\n")
for track in self.tracks:
fp.write(" <trk>\n <trkseg>\n")
for node in track.nodes:
fp.write(' <trkpt lat="%s" lon="%s" />\n' % (node.lat, node.lon))
fp.write(" </trkseg>\n </trk>\n")
fp.write("</gpx>")
fp.close()
class GPXParser(xml.sax.ContentHandler):
"""
Parses GPX files from the OSM GPS trackpoint downloader. Converts them to OSM format
"""
def __init__(self, containing_obj):
self.tracks = []
self.__current_way = None
self.containing_obj = containing_obj
def startElement(self, name, attrs):
if name == "trkseg":
self.__current_way = Way()
elif name == "trkpt":
assert self.__current_way is not None, "Invalid GPX file, we've encountered a trkpt tag before a trkseg tag"
self.__current_way.nodes.append(Node(lat=attrs['lat'], lon=attrs['lon']))
def endElement(self, name):
if name == 'trkseg':
self.containing_obj.tracks.append(self.__current_way)
self.__current_way = None
class OSMServer(object):
def __init__(self, api_root):
self.api_root = api_root
def _get_data(self, subpath):
if subpath[0] != '/' and self.api_root[-1] != '/':
# print a warning?
pass
return urllib2.urlopen(self.api_root + subpath).read()
def node(self, node_id):
osm_data = OSMXMLFile(self._get_data("node/%s" % node_id))
if len(osm_data.nodes) == 0:
# no nodes found
return None
elif len(osm_data.nodes) == 1:
return osm_data.nodes[0]
def way(self, way_id):
osm_data = OSMXMLFile(self._get_data("way/%s" % way_id))
if len(osm_data.ways) == 0:
# no ways found
return None
elif len(osm_data.ways) == 1:
return osm_data.ways[0]
def relation(self, relation_id):
osm_data = OSMXMLFile(self._get_data("relation/%s" % relation_id))
if len(osm_data.relations) == 0:
# no relations found
return None
elif len(osm_data.relations) == 1:
return osm_data.relations[0]
# Mark Pilgram's excellent openAnything
def open_anything(source):
# try to open with urllib (if source is http, ftp, or file URL)
try:
return urllib.urlopen(source)
except (IOError, OSError):
pass
# try to open with native open function (if source is pathname)
try:
return open(source)
except (IOError, OSError):
pass
# treat source as string
return StringIO.StringIO(str(source))
def deg_to_tiles(lat_deg, lon_deg, zoom):
lat_rad = lat_deg * math.pi / 180.0
n = 2.0 ** zoom
xtile = int((lon_deg + 180.0) / 360.0 * n)
ytile = int((1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n)
return (xtile, ytile)
def tiles_to_deg(xtile, ytile, zoom):
n = 2.0 ** zoom
lon_deg = xtile / n * 360.0 - 180.0
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
lat_deg = lat_rad * 180.0 / math.pi
return (lat_deg, lon_deg)
osm_web = OSMServer("http://api.openstreetmap.org/api/0.5/")