Package mosp :: Package geo :: Module osm
[hide private]
[frames] | no frames]

Source Code for Module mosp.geo.osm

  1  # -*- coding: utf-8 -*- 
  2  """Loading OSM XML data and storing it into an OSMModel. 
  3   
  4  OSM XML data is loaded and manipulated via OSMXMLFileParser  
  5  within OSMModel. An OSMModel stores the OSM data for simulation. 
  6  These and subordinated classes are originally built upon code from  
  7  https://github.com/rory/python-osm 
  8   
  9  """ 
 10   
 11  from __future__ import absolute_import 
 12   
 13  import logging 
 14  import time 
 15  import xml.sax 
 16  import math 
 17   
 18  from . import utm 
 19  from mosp import routing 
 20  from mosp import collide 
 21   
 22  __author__ = "B. Henne, F. Ludwig, P. Tute, R. McCann" 
 23  __contact__ = "henne@dcsec.uni-hannover.de" 
 24  __copyright__ = "(c) 2008-2011 Rory McCann, 2010-2012, DCSec, Leibniz Universitaet Hannover, Germany" 
 25  __license__ = "GPLv3" 
 26   
 27  GLOBAL_SIM = None 
 28   
 29  ROADTYPE_NODIMENSION = 0            #: constant/no road dimension - road width = 0 
 30  ROADTYPE_ONEWAY_NOSIDEWALK = 1      #: constant/road width = width from the middle of road to the right in walking direction (as int) 
 31  ROADTYPE_TWOWAY_NOSIDEWALK = 2      #: constant/road width = 2xwidth from the left of the road to the right both directions lanes (as int) 
 32  ROADTYPE_ONEWAY_ONSIDEWALK = 3      #: constant/no movement on street, but only on sidewalk (as list [road width per direction, sidewalk width+road width per direction] 
 33   
 34  ROADTYPE = ROADTYPE_NODIMENSION     #: used ROADTYPE model 
 35   
 36  ROADWIDTH_DEFAULTS = { 'footway':2, 'service':2, 'tertiary':3, 'secondary':4, 'primary':4, 'else':2} #: defaults for roads without any width tags 
 37   
 38   
39 -def round_utm_coord(x):
40 """Rounds UTM coordinate to demanded precision. 41 42 Rounds value x up to 3 decimal places. UTM value precision down to millimeter. 43 @author: B. Henne""" 44 return round(x, 3)
45 46
47 -class Node(routing.RoutingNode):
48 """A geo-model node. 49 @author: P. Tute 50 @author: Rory McCann""" 51
52 - def __init__(self, id=None, x=None, y=None, zone=None, lon=None, lat=None, tags=None):
53 """Initialize the geo-model node. 54 55 @param id: node id 56 @param x: UTM easting 57 @param y: UTM northing 58 @param zone: UTM zone 59 @param lon: WGS84 Longitude 60 @param lat: WGS84 Latitude 61 @param tags: OSM tags""" 62 63 self._x = None # UTM easting 64 self._y = None # UTM northing 65 self._z = None # UTM zone 66 self._lon = None # WGS84 Longitude 67 self._lat = None # WGS84 Latitude 68 69 if lon is not None and lat is not None: 70 self._lon, self._lat = float(lon), float(lat) 71 self._z = utm.long_to_zone(self._lon) 72 self._x, self._y = utm.latlong_to_utm(self._lon, 73 self._lat, 74 utm.long_to_zone(self._lon)) 75 76 if x is not None and y is not None and zone is not None: 77 self._x, self._y, self._z = x, y, zone 78 self._lon, self._lat = utm.utm_to_latlong(x, y, zone) 79 80 self._x = round_utm_coord(self._x) 81 self._y = round_utm_coord(self._y) 82 83 if tags: 84 self.tags = tags 85 else: 86 self.tags = {} 87 88 routing.RoutingNode.__init__(self, int(id)) 89 a = ""
90
91 - def getLat(self):
92 """Returns the node's Latitude.""" 93 return self._lat
94
95 - def getLon(self):
96 """Returns the nodes's Longitude.""" 97 return self._lon
98
99 - def setLat(self, lat):
100 """Sets the node's Latitude and UTM coordinates.""" 101 self._lat = lat 102 self._x, self._y = utm.latlong_to_utm(self._lon, 103 self._lat, 104 utm.long_to_zone(self._lon)) 105 self._x = round_utm_coord(self._x) 106 self._y = round_utm_coord(self._y)
107 108
109 - def setLon(self, lon):
110 """Sets the node's Longitude and UTM coordinates.""" 111 self._lon = lon 112 self._x, self._y = utm.latlong_to_utm(self._lon, 113 self._lat, 114 utm.long_to_zone(self._lon)) 115 self._x = round_utm_coord(self._x) 116 self._y = round_utm_coord(self._y)
117 118 119 lat = property(getLat, setLat) 120 lon = property(getLon, setLon) 121
122 - def getX(self):
123 """Returns the node's x coordinate, it's UTM easting.""" 124 return self._x
125
126 - def getY(self):
127 """Returns the node's y coordinate, it's UTM northing.""" 128 return self._y
129
130 - def getZone(self):
131 """Returns the node's UTM zone.""" 132 return self._z
133
134 - def setX(self, x):
135 """Sets the node's x coordinate (UTM easting) and re-calculates Lat/Lon.""" 136 self._x = round_utm_coord(x) 137 self.lon, self.lat = utm.utm_to_latlong(self._x, self._y, self._z)
138
139 - def setY(self, y):
140 """Sets the node's y coordinate (UTM northing) and re-calculates Lat/Lon.""" 141 self._y = round_utm_coord(y) 142 self.lon, self.lat = utm.utm_to_latlong(self._x, self._y, self._z)
143
144 - def setZone(self, z):
145 """Sets the node's y coordinate (UTM northing) and re-calculates Lat/Lon.""" 146 self._z = round_utm_coord(z) 147 self.lon, self.lat = utm.utm_to_latlong(self._x, self._y, self._z)
148 149 x = property(getX, setX) 150 y = property(getY, setY) 151 z = property(getZone, setZone) 152
153 - def __repr__(self):
154 n = ['Node(id=%r, distance=%f)' % (n.id, d) for n, d in self.neighbors.items()] 155 return "Node(id=%r, lon=%r, lat=%r, tags=%r, neighbors=%r)"\ 156 % (self.id, self.lon, self.lat, self.tags, n)
157 158
159 -class Way(object):
160 """A geo-model way. 161 @author: Rory McCann"""
162 - def __init__(self, id, nodes=None, tags=None):
163 self.id = id 164 if nodes: 165 self.nodes = nodes 166 else: 167 self.nodes = [] 168 if tags: 169 self.tags = tags 170 else: 171 self.tags = {}
172
173 - def __repr__(self):
174 return "Way(id=%r, nodes=%r, tags=%r)" % (self.id, self.nodes, self.tags)
175 176
177 -def wayangle(src_node, dest_node):
178 """Calculates the angle between the way from src_node to dest_node and positive x-axis of a plane. 179 @author: B. Henne""" 180 y = dest_node.y - src_node.y 181 x = dest_node.x - src_node.x 182 return math.ceil(math.atan2(y,x)/math.pi*180)
183 184
185 -class WaySegment(collide.Line):
186 """A geo-model way segment, the line between two nodes 187 @author: B. Henne 188 @author: P. Tute"""
189 - def __init__(self, node0, node1, width=None, tags=None):
190 """Initializes WaySegment, see update method.""" 191 self.nodes = [None, None] 192 self.width = [0,0] 193 self.tags = {} 194 self.persons = [] 195 self.update(node0, node1, width, tags)
196
197 - def update(self, node0=None, node1=None, width=None, tags=None):
198 """Updates WaySegment parameters. 199 200 @param node0: start node of the segment 201 @param node1: end node of the segment 202 @param width: is road width at this WaySegment 203 @param tags: carries corresponding osm way tags""" 204 205 if node0: 206 self.nodes[0] = node0 207 if node1: 208 self.nodes[1] = node1 209 if width: 210 if (ROADTYPE == ROADTYPE_NODIMENSION): 211 self.width = [0,0] 212 elif (ROADTYPE == ROADTYPE_ONEWAY_NOSIDEWALK): 213 self.width = [0,width] 214 elif (ROADTYPE == ROADTYPE_TWOWAY_NOSIDEWALK): 215 self.width = [-width,width] 216 elif (ROADTYPE == ROADTYPE_ONEWAY_ONSIDEWALK): 217 assert len(width) == 2 218 self.width = width 219 else: 220 assert len(width) == 2 221 self.width = width 222 if tags: 223 self.tags = tags 224 node0 = self.nodes[0] 225 node1 = self.nodes[1] 226 self.directions = {node0:wayangle(node1, node0), node1:wayangle(node0, node1)} 227 super(WaySegment, self).__init__(node0.x, node0.y, node1.x, node1.y)
228
229 - def __cmp__(self, o):
230 """Compare WaySegments by id""" 231 return cmp(self.id, o.id)
232
233 - def __repr__(self):
234 #return "<Line %i from (%i,%i) to (%i, %i)>" % ( 235 # self.id, self.x_start, self.y_start, self.x_end, self.y_end) 236 return str(self.id)
237 238
239 -class NodePlaceHolder(object):
240 """Placeholder for OSM nodes while parsing the OSM data. 241 242 NodePlaceHolder later is replaced by (references to) Nodes. 243 @author: Rory McCann"""
244 - def __init__(self, id):
245 """Inits the NodePlaceHolder""" 246 self.id = id
247
248 - def __repr__(self):
249 return "NodePlaceHolder(id=%r)" % (self.id)
250 251
252 -def calc_width(way):
253 """Calculates the width of a way based on osm tags and width defaults. 254 @author: B. Henne 255 @todo: add more road width calculation magic""" 256 if 'width' in way.tags: 257 re = way.tags['width'].replace(',','.',1) 258 elif 'approx_width' in way.tags: 259 re = way.tags['approx_width'].replace(',','.',1) 260 #TODO: more magic here 261 elif 'highway' in way.tags: 262 if way.tags['highway'] in ROADWIDTH_DEFAULTS: 263 re = ROADWIDTH_DEFAULTS[way.tags['highway']] 264 else: 265 re = ROADWIDTH_DEFAULTS['else'] 266 return float(re)
267 268
269 -def distance(node1, node2):
270 """Calculates the euclidean distance of two nodes in the plane. 271 272 Calculates the distance of two nodes described by UTM coordinates x,y 273 in the area by the euclidean distance using the Pythagorean theorem. 274 @author: B. Henne""" 275 x = node1.x - node2.x 276 y = node1.y - node2.y 277 return math.sqrt(x**2 + y**2)
278 279
280 -class OSMModel(collide.World):
281 """A OSM-based geo-model. A simulation world with geo data. 282 @author: F. Ludwig 283 @author: P. Tute 284 @author: B. Henne"""
285 - def __init__(self, fname, **kwargs):
286 """Initializes OSMModel object. 287 288 Call initialize() to load OSM XML data from fname.""" 289 super(OSMModel, self).__init__(**kwargs) 290 self.fobj = open(fname) 291 self.path = fname 292 self.nodes = {} 293 self.ways = {}
294
295 - def out_of_bb(self, node):
296 """Is node out of UTM bounding box?""" 297 x = round_utm_coord(node.x) 298 y = round_utm_coord(node.y) 299 min_x = round_utm_coord(self.bounds["min_x"]) 300 max_x = round_utm_coord(self.bounds["max_x"]) 301 min_y = round_utm_coord(self.bounds["min_y"]) 302 max_y = round_utm_coord(self.bounds["max_y"]) 303 return (x < min_x or 304 x > max_x or 305 y < min_y or 306 y > max_y)
307
308 - def initialize(self, sim, enable_routing=True):
309 """Initializes the model by parsing and manipulating OSM XML data.""" 310 #parse osm file 311 parser = xml.sax.make_parser() 312 handler = OSMXMLFileParser(self) 313 parser.setContentHandler(handler) 314 parser.parse(self.fobj) 315 316 assert self.bounds["minlon"] < self.bounds["maxlon"] 317 assert self.bounds["minlat"] < self.bounds["maxlat"] 318 319 # recalculate bounding box as utm 320 self.bounds["min_x"], self.bounds["min_y"] = utm.latlong_to_utm(self.bounds["minlon"], 321 self.bounds["minlat"]) 322 self.bounds["max_x"], self.bounds["max_y"] = utm.latlong_to_utm(self.bounds["maxlon"], 323 self.bounds["maxlat"]) 324 325 # now fix up all the references - replace NodePlaceHolders with Nodes 326 for way in self.ways.values(): 327 way.nodes = [self.nodes[handler.node_id_map[node_pl.id]] 328 for node_pl in way.nodes] 329 330 # these maps are on and off needed for debugging 331 # BUG: ok for non_way_nodes, broken for way_nodes 332 #self.map_osmnodeid_nodeid = handler.node_id_map 333 #self.map_nodeid_osmnodeid = {} 334 #for k, v in self.map_osmnodeid_nodeid.iteritems(): 335 # self.map_nodeid_osmnodeid[v] = k 336 337 # free mem 338 del handler 339 340 # connect nodes as neighbors and with ways 341 for j, way in enumerate(self.ways.values()): 342 for i in xrange(len(way.nodes)-1): 343 current_node = way.nodes[i] 344 next_node = way.nodes[i+1] 345 346 # create WaySegments between current neighbors 347 way_segment = WaySegment(current_node, next_node, width=calc_width(way), tags=way.tags) 348 way_segment.id = i + 1000 * j 349 self.add(way_segment) 350 next_node.ways[current_node] = way_segment 351 current_node.ways[next_node] = way_segment 352 353 # mark nodes as neighbors and calculate their distances 354 current_node.neighbors[next_node] = int(distance(current_node, next_node)) 355 next_node.neighbors[current_node] = int(distance(current_node, next_node)) 356 del self.ways 357 358 # distinguish between way_nodes and non_way_nodes based on their neighbors 359 self.non_way_nodes = [] 360 self.way_nodes = [] 361 for n in self.nodes.values(): 362 # nodes with neighbors are on ways 363 if n.neighbors: 364 self.way_nodes.append(n) 365 # exclude nodes without neighbors and without any tag 366 elif len(n.tags) > 0: 367 self.non_way_nodes.append(n) 368 369 t = time.time() 370 371 # filter objects contained in World for collide.Lines 372 ways = sorted(filter(lambda w: isinstance(w, collide.Line), list(self.obj))) 373 374 x_min, y_min = self.bounds["min_x"], self.bounds["min_y"] 375 x_max, y_max = self.bounds["max_x"], self.bounds["max_y"] 376 # check for lines colliding with west, east, north and south border 377 for func, arg0, arg1, arg2 in ((collide.Line.collide_vertical_line, x_min, y_min, y_max), # west 378 (collide.Line.collide_vertical_line, x_max, y_min, y_max), # east 379 (collide.Line.collide_horizontal_line, x_min, x_max, y_max), # north 380 (collide.Line.collide_horizontal_line, x_min, x_max, y_min)): # south 381 for way in ways: 382 collision = func(way, arg0, arg1, arg2) 383 colliding = False 384 if collision[0]: 385 # remove WaySegments that intersect two times since they are useless 386 if (self.out_of_bb(way.nodes[0]) and 387 self.out_of_bb(way.nodes[1])): 388 # kind of awkward removal of ways...somehow wrong nodes will be removed otherwise 389 self.obj = set([w for w in self.obj if not w.id == way.id]) 390 #self.obj.remove(way) 391 if way.nodes[0] in self.way_nodes: 392 self.way_nodes.remove(way.nodes[0]) 393 if way.nodes[1] in self.way_nodes: 394 self.way_nodes.remove(way.nodes[1]) 395 if way.nodes[1] in way.nodes[0].neighbors.keys(): 396 del way.nodes[0].neighbors[way.nodes[1]] 397 if way.nodes[0] in way.nodes[1].neighbors.keys(): 398 del way.nodes[1].neighbors[way.nodes[0]] 399 continue 400 for node in way.nodes: 401 # move nodes outside of bounding box onto bounding box 402 if node.x < x_min: 403 node.x = collision[1] 404 node.y = collision[2] 405 colliding = True 406 side = "west" 407 elif node.x > x_max: 408 node.x = collision[1] 409 node.y = collision[2] 410 colliding = True 411 side = "east" 412 if node.y < y_min: 413 node.x = collision[1] 414 node.y = collision[2] 415 colliding = True 416 side = "south" 417 elif node.y > y_max: 418 node.x = collision[1] 419 node.y = collision[2] 420 colliding = True 421 side = "north" 422 if colliding: 423 node.tags["border"] = side 424 break 425 # fix distances and update way (fixes coordinates and angle) 426 start_node = way.nodes[0] 427 end_node = way.nodes[1] 428 dist = int(distance(start_node, end_node)) 429 start_node.neighbors[end_node] = dist 430 end_node.neighbors[start_node] = dist 431 way.update() 432 elif (self.out_of_bb(way.nodes[0]) 433 and self.out_of_bb(way.nodes[1]) and 434 way in self.obj): 435 self.obj.remove(way) 436 # remove nodes outside of bounding box 437 for node in self.way_nodes[:]: 438 # check if node is outside bb 439 if self.out_of_bb(node): 440 self.way_nodes.remove(node) 441 # and check if any neighbors need to be removed 442 # (if not done, routing may break!) 443 for neighbor in node.neighbors.keys(): 444 if self.out_of_bb(neighbor): 445 del node.neighbors[neighbor] 446 447 # remove non_way_nodes outside of bb (those cannot be reached anyways) 448 for node in self.non_way_nodes[:]: 449 # check if node is outside bb 450 if self.out_of_bb(node): 451 self.non_way_nodes.remove(node) 452 453 # perform a sanity-check on ways, remove ways with less than two correct nodes 454 ways = sorted(filter(lambda w: isinstance(w, collide.Line), list(self.obj))) 455 for way in ways: 456 if (way.nodes[0] not in self.way_nodes or 457 way.nodes[1] not in self.way_nodes): 458 self.obj = set([w for w in self.obj if not w.id == way.id]) 459 460 # sort way_nodes and close gaps in IDs 461 self.way_nodes = sorted(self.way_nodes, key=lambda n:n.id) 462 for i, node in enumerate(self.way_nodes): 463 node.id = i 464 465 pass # replaces next logging statement 466 #logging.debug('created borders %.2f' % (time.time() - t)) 467 468 if enable_routing: 469 t = time.time() 470 routing.calc(self.way_nodes, self.path[:-4]) 471 #routing.calc(self.nodes, self.path[:-4]+'_exits', setup=True) # setup=True as quick fix for broken routing data after adding exit nodes => TODO: fix later 472 pass # replaces next logging statement 473 #logging.debug('routing.calc 2 %.2fs' % (time.time() - t)) 474 475 t = time.time() 476 self.calculate_grid(cache_base_path=self.path[:-4]) 477 pass # replaces next logging statement 478 #logging.debug('calculate_grid colliding %.2fs' % (time.time() - t)) 479 480 if not enable_routing: 481 for node in self.way_nodes: 482 node.neighbors = {} 483 484 self.way_nodes_by_id = {} 485 for node in self.way_nodes: 486 self.way_nodes_by_id[node.id] = node 487 488 # these maps are on and off needed 489 # fixed new implementation 490 self.map_nodeid_osmnodeid = {} 491 self.map_osmnodeid_nodeid = {} 492 for n in self.way_nodes: 493 self.map_nodeid_osmnodeid[n.id] = n.osm_id 494 self.map_osmnodeid_nodeid[n.osm_id] = n.id
495 496
497 -class OSMXMLFileParser(xml.sax.ContentHandler):
498 """The OSM XML parser. 499 500 Parses OSM data. highway_blacklist can be used to filter it. 501 @author: F. Ludwig 502 @author: B. Henne 503 @author: Rory McCann"""
504 - def __init__(self, containing_obj):
505 self.containing_obj = containing_obj 506 self.bounds = {} 507 self.zone = 0 508 self.curr_node = None 509 self.curr_way = None 510 self.node_id = 0 511 self.node_id_map = {} 512 self.highway_blacklist = ['motorway','motorway_link', # German Autobahnen 513 'trunk', 'trunk_link', # German Autobahnähnliche Straßen 514 #'primary', # German Bundesstraßen 515 #'secondary', # German Landesstraßen 516 #'tertiary', # German Kreisstraßen 517 #'unclassified', # German Gemeindestraßen 518 #'service', # German Zufahrtswege 519 #'track', # German Waldwege 520 ]
521
522 - def startElement(self, name, attrs):
523 """If Opening a xml element ...""" 524 if name == 'bounds': 525 if self.bounds == {}: 526 self.bounds['minlat'] = float(attrs['minlat']) 527 self.bounds['minlon'] = float(attrs['minlon']) 528 self.bounds['maxlat'] = float(attrs['maxlat']) 529 self.bounds['maxlon'] = float(attrs['maxlon']) 530 elif name == 'bound': 531 if self.bounds == {}: 532 box = attrs['box'].split(',') 533 self.bounds['minlat'] = float(box[0]) 534 self.bounds['minlon'] = float(box[1]) 535 self.bounds['maxlat'] = float(box[2]) 536 self.bounds['maxlon'] = float(box[3]) 537 elif name == 'node': 538 if not ((attrs.has_key('action')) and (attrs['action'] == 'delete')): 539 self.node_id_map[attrs['id']] = self.node_id 540 self.curr_node = Node(id=self.node_id, lon=attrs['lon'], lat=attrs['lat']) 541 self.curr_node.osm_id = attrs['id'] 542 self.node_id += 1 543 else: 544 self.curr_node = None 545 elif name == 'way': 546 if not ((attrs.has_key('action')) and (attrs['action'] == 'delete')): 547 self.curr_way = Way(id=attrs['id']) 548 else: 549 self.curr_way = None 550 elif name == 'tag': 551 if self.curr_node: 552 self.curr_node.tags[attrs['k']] = attrs['v'] 553 elif self.curr_way: 554 self.curr_way.tags[attrs['k']] = attrs['v'] 555 elif name == "nd": 556 assert self.curr_node is None, "curr_node (%r) is non-none" % (self.curr_node) 557 assert self.curr_way is not None, "curr_way is None" 558 self.curr_way.nodes.append(NodePlaceHolder(id=attrs['ref']))
559
560 - def endElement(self, name):
561 """If closing the xml element ...""" 562 if name == "bounds" or name == "bound": 563 self.containing_obj.bounds = self.bounds 564 self.containing_obj.zone = self.zone = int(utm.long_to_zone(self.bounds['minlon']+((self.bounds['maxlon']-self.bounds['minlon'])/2))) 565 elif name == "node": 566 if self.curr_node is not None: 567 self.containing_obj.nodes[self.curr_node.id] = self.curr_node 568 self.curr_node = None 569 elif name == "way": 570 if self.curr_way is not None: 571 if 'highway' in self.curr_way.tags: # TODO check osm documentation 572 self.containing_obj.ways[self.curr_way.id] = self.curr_way 573 self.curr_way = None
574 # *.routes.bz2 must be recalculated otherwise this code does not work - this codes replaces above 3 lines 575 # if not (('motorroad' in self.curr_way.tags) and (self.curr_way.tags['motorroad'] == 'yes')): # exclude German Kraftfahrtstraßen 576 # if 'highway' in self.curr_way.tags: 577 # if self.curr_way.tags['highway'] not in self.highway_blacklist: 578 # self.containing_obj.ways[self.curr_way.id] = self.curr_way 579 # self.curr_way = None 580