def match_point(self, s): if isinstance(s, str): try: point = Point.from_string(s) except ValueError: return None normalized_point = point if self.is_lat_long(point): point = point.reproject() geom = func.ST_GeomFromText(point.wkt, DEFAULT_SRID) distance = func.ST_Distance(geom, Intersection.geom) distance = distance.label('distance') # Try to get an Intersection first q = self.session.query(Intersection, distance) q = q.filter(distance < 5) # 5 meters (make configurable) q = q.order_by(distance) result = q.first() if result is not None: closest_object = result.Intersection closest_point = closest_object.geom else: # Otherwise, get a Street distance = func.ST_Distance(geom, Street.geom).label('distance') q = self.session.query(Street, distance) q = q.order_by(distance) closest_object = q.first().Street # Get point on Street closest to input point closest_point = func.ST_ClosestPoint(Street.geom, geom) closest_point = closest_point.label('closest_point') q = self.session.query(closest_point).select_from(Street) q = q.filter_by(id=closest_object.id) closest_point = q.scalar() closest_point = Point.from_wkb(closest_point) name = closest_object.name return LookupResult(s, normalized_point, closest_point, closest_object, name)
def process_nodes(self): """Process nodes""" # All the nodes encountered so far. encountered = set() # Nodes at the start or end of a way AND nodes that are shared # between two or more ways. These will be inserted into the DB. intersections = set() # Go through all the ways and find all the intersection nodes. for el in self.iter_ways(): node_ids = el['nodes'] # This is necessary in case a start or end node isn't shared # with any other ways. E.g., at a dead end or at the data # boundary. encountered |= {node_ids[0], node_ids[-1]} for node_id in node_ids: if node_id in encountered: intersections.add(node_id) else: encountered.add(node_id) del encountered rows = [] def insert(): self.engine.execute(NODE_TABLE.insert(), rows) rows.clear() self.engine.execute(NODE_TABLE.delete()) self.engine.execute(INTERSECTION_TABLE.delete()) for el in self.iter_nodes(): osm_id = el['id'] latitude = el['lat'] longitude = el['lon'] lat_long = Point(longitude, latitude) geom = lat_long.reproject() node = { 'id': osm_id, 'is_intersection': osm_id in intersections, 'geom': geom, } rows.append(node) if len(rows) > 1000: insert() del intersections if rows: insert() self.engine.execute(""" INSERT INTO {to_table} (id, geom) SELECT id, geom FROM {from_table} WHERE is_intersection """.format(from_table=Node.__tablename__, to_table=Intersection.__tablename__))
def match_point(self, s): if isinstance(s, str): try: point = Point.from_string(s) except ValueError: return None normalized_point = point geom = func.ST_GeomFromText(point.wkt, DEFAULT_SRID) distance = func.ST_Distance( func.ST_GeogFromWKB(geom), func.ST_GeogFromWKB(Intersection.geom)) distance = distance.label('distance') # Distance threshold in meters # TODO: Should this be scale-dependent? distance_threshold = self.config.get('distance_threshold', 10) # Try to get an Intersection first q = self.session.query(Intersection, distance) q = q.filter(distance < distance_threshold) q = q.order_by(distance) result = q.first() if result is not None: closest_object = result.Intersection closest_point = closest_object.geom name = closest_object.name else: # Otherwise, get a Street distance = func.ST_Distance(geom, Street.geom).label('distance') q = self.session.query(Street, distance) q = q.filter( Street.highway.in_(Street.routable_types) | Street.bicycle.in_(Street.bicycle_allowed_types) ) q = q.order_by(distance) closest_object = q.first().Street # Get point on Street closest to input point closest_point = func.ST_ClosestPoint(Street.geom, geom) closest_point = closest_point.label('closest_point') q = self.session.query(closest_point).select_from(Street) q = q.filter_by(id=closest_object.id) closest_point = q.scalar() closest_point = Point.from_wkb(closest_point) name = closest_object.display_name return LookupResult(s, normalized_point, closest_point, closest_object, name, 'byCycle point')
def _mapbox_result_to_lookup_result(self, s, result): name = result['place_name'].rsplit(', ', 1)[0] # Remove country long, lat = result['center'] geom = Point(long, lat) closest_object = self.match_point(f'{lat},{long}').closest_object data = { 'relevance': result['relevance'], 'score': result.get('score'), # Mapbox prominence score } return LookupResult(s, name, geom, closest_object, name, 'Mapbox', data)
def trim_line(line, point1, point2): distance1 = line.project(point1) distance2 = line.project(point2) if distance1 > distance2: point1, point2 = point2, point1 distance1, distance2 = distance2, distance1 coords = [point1] for c in line.coords: p = Point(c) p_distance = line.project(p) if distance1 <= p_distance <= distance2: coords.append(c) coords.append(point2) return LineString(coords)
def match_id(self, s): match = ID_RE.search(s) if match: type_ = match.group('type') if type_ not in TYPE_MAP: raise InputError('Unknown type: %s' % type_) type_ = TYPE_MAP[type_] obj = self.session.query(type_).get(match.group('id')) if obj is None: return None if isinstance(obj, Intersection): geom = obj.geom else: length = obj.geom.length geom = Point(obj.geom.interpolate(length / 2)) return LookupResult(s, obj, geom, obj, obj.name, 'byCycle ID')
def split(self, point, node_id, way1_id, way2_id): """Split this street at ``point``.""" distance = self.geom.project(point) coords = self.geom.coords shared_coords = list(point.coords) coords1 = [coords[0]] coords2 = [] for c in coords[1:-1]: p = Point(c) p_distance = self.geom.project(p) if p_distance < distance: coords1.append(c) elif p_distance > distance: coords2.append(c) coords2.append(coords[-1]) coords1 = coords1 + shared_coords coords2 = shared_coords + coords2 line1 = LineString(coords1) line2 = LineString(coords2) shared_node = Intersection(id=node_id, geom=point) # Start to shared node way1 = self.clone( id=way1_id, end_node_id=node_id, end_node=shared_node, geom=line1, ) # Shared node to end way2 = self.clone( id=way2_id, start_node_id=node_id, start_node=shared_node, geom=line2, ) shared_node.ways = [way1, way2] return shared_node, way1, way2
def split_line(line, point): """Split linestring at point.""" distance = line.project(point) coords = line.coords shared_coords = list(point.coords) coords1 = [coords[0]] coords2 = [] for c in coords[1:-1]: p = Point(c) p_distance = line.project(p) if p_distance < distance: coords1.append(c) elif p_distance > distance: coords2.append(c) coords2.append(coords[-1]) coords1 = coords1 + shared_coords coords2 = shared_coords + coords2 return LineString(coords1), LineString(coords2)
def make_directions(self, node_ids, edge_ids, split_edges): """Process the shortest path into a nice list of directions. ``node_ids`` The IDs of the nodes on the route ``edges_ids`` The IDs of the edges on the route ``split_edges`` Temporary edges formed by splitting an existing edge when the start and/or end of a route is within an edge (e.g., for an address like "123 Main St") return * A list of directions. Each direction has the following form:: { 'turn': 'left', 'name': 'SE Stark St', 'type': 'residential', 'toward': 'SE 45th Ave', 'distance': { 'meters': 80.0, 'kilometers': 0.08, 'feet': 264.0, 'miles': 0.05, }, 'point': (-122.5, 45.5), 'edge_ids': [1, 2, 3, 4], } * A linestring, which is a list of x, y coords: [(x, y), ...] * A `dict` of total distances in units of meters, kilometers, feet, and miles: { 'meters': 1110.0, 'kilometers': 1.11, 'feet': 5487.0, 'miles': 1.04, } """ edges = [] synthetic_start_edge = edge_ids[0] < 0 synthetic_end_edge = len(edge_ids) > 1 and edge_ids[-1] < 0 if synthetic_start_edge: edges.append(split_edges[edge_ids[0]]) i = 1 if synthetic_start_edge else None j = -1 if synthetic_end_edge else None filter_ids = edge_ids[i:j] if i or j else edge_ids if filter_ids: q = self.session.query(Street).filter(Street.id.in_(filter_ids)) q = q.options(joinedload(Street.start_node)) q = q.options(joinedload(Street.end_node)) edge_map = {edge.id: edge for edge in q} edges.extend(edge_map[edge_id] for edge_id in filter_ids) if synthetic_end_edge: edges.append(split_edges[edge_ids[-1]]) directions = [] stretches = [] prev_name = None linestring_points = [] total_distance = 0 for edge, next_edge, next_node_id in zip(edges, chain(edges[1:], [None]), node_ids[1:]): name = edge.name geom = edge.geom length = edge.meters reverse_geom = edge.start_node_id == next_node_id points = geom.coords[::-1] if reverse_geom else geom.coords start_bearing = self.get_bearing(*points[:2]) end_bearing = self.get_bearing(*points[-2:]) if next_edge is None: # Reached last edge linestring_points.extend(points) else: linestring_points.extend(points[:-1]) total_distance += length if name and name == prev_name: stretch = stretches[-1] stretch['edges'].append(edge) stretch['length'] += length stretch['end_bearing'] = end_bearing else: # Start of a new stretch stretches.append({ 'edges': [edge], 'length': length, 'toward_node_id': next_node_id if next_node_id > -1 else None, 'point': Point(*points[0]), 'start_bearing': start_bearing, 'end_bearing': end_bearing, }) prev_name = name # Create directions from stretches. for prev_stretch, stretch in zip([None] + stretches[:-1], stretches): first_edge = stretch['edges'][0] length = stretch['length'] start_bearing = stretch['start_bearing'] toward_node_id = stretch['toward_node_id'] if prev_stretch is None: # First edge in stretch turn = self.get_direction_from_bearing(start_bearing) else: # Remaining edges prev_end_bearing = prev_stretch['end_bearing'] turn = self.calculate_way_to_turn(prev_end_bearing, start_bearing) direction = { 'turn': turn, 'name': first_edge.display_name, 'type': first_edge.highway, 'toward': toward_node_id, 'distance': self.distance_dict(length), 'point': stretch['point'], 'edge_ids': [edge.id for edge in stretch['edges']] } directions.append(direction) # Get the toward street at the start of each stretch found in # the loop just above. This is deferred to here so that we can # fetch all the toward nodes up front with their associated # edges in a single query. This is much faster than processing # each node individually inside the loop--that causes up to 2*N # additional queries being issued to the database (fetching of # the inbound and outbound edges for the node). filter_ids = [direction['toward'] for direction in directions] if filter_ids: q = self.session.query(Intersection) q = q.filter(Intersection.id.in_(filter_ids)) q = q.options(joinedload(Intersection.streets)) node_map = {node.id: node for node in q} else: node_map = {} for direction in directions: name = direction['name'] toward_node_id = direction['toward'] if toward_node_id is None: # This is a special case where the destination is within # an edge (i.e., it's a street address) AND there are no # intersections between the last turn and the # (synthetic) destination node. In this case, since the # destination node doesn't have any intersecting edges, # a toward street can't be determined. Also, the # destination node won't have been fetched in the query # above because it doesn't really exist. toward = 'your destination' else: node = node_map[toward_node_id] toward = self.get_different_name_from_intersection(name, node) direction['toward'] = toward # TODO: Extract jogs? linestring = LineString(linestring_points) distance = self.distance_dict(total_distance) return directions, linestring, distance