team-3/src/c3nav/routing/router.py

408 lines
16 KiB
Python
Raw Normal View History

2017-11-27 15:01:58 +01:00
import operator
2017-11-27 13:21:53 +01:00
import os
import pickle
from collections import deque
2017-11-27 15:01:58 +01:00
from functools import reduce
from itertools import chain
import numpy as np
2017-11-27 13:21:53 +01:00
from django.conf import settings
from django.utils.functional import cached_property
2017-11-27 15:01:58 +01:00
from scipy.sparse.csgraph._shortest_path import shortest_path
from shapely import prepared
2017-11-28 22:02:24 +01:00
from shapely.geometry import LineString, Point
from shapely.ops import unary_union
from c3nav.mapdata.models import AltitudeArea, Area, GraphEdge, Level, LocationGroup, Space, WayType
2017-11-28 22:40:01 +01:00
from c3nav.mapdata.models.geometry.space import POI
2017-11-28 22:02:24 +01:00
from c3nav.mapdata.utils.geometry import assert_multipolygon, good_representative_point
2017-11-27 15:01:58 +01:00
from c3nav.routing.route import Route
class Router:
2017-11-27 13:21:53 +01:00
filename = os.path.join(settings.CACHE_ROOT, 'router')
2017-11-28 22:40:01 +01:00
def __init__(self, levels, spaces, areas, pois, groups, nodes, edges, waytypes, graph):
2017-11-27 13:21:53 +01:00
self.levels = levels
self.spaces = spaces
self.areas = areas
2017-11-28 22:40:01 +01:00
self.pois = pois
self.groups = groups
2017-11-27 13:21:53 +01:00
self.nodes = nodes
2017-11-27 15:01:58 +01:00
self.edges = edges
2017-11-27 13:21:53 +01:00
self.waytypes = waytypes
self.graph = graph
@staticmethod
def get_altitude_in_areas(areas, point):
return max(area.get_altitudes(point)[0] for area in areas if area.geometry_prep.intersects(point))
@classmethod
2017-11-27 16:53:56 +01:00
def rebuild(cls):
levels_query = Level.objects.prefetch_related('buildings', 'spaces', 'altitudeareas', 'groups',
'spaces__holes', 'spaces__columns', 'spaces__groups',
'spaces__obstacles', 'spaces__lineobstacles',
2017-11-28 22:40:01 +01:00
'spaces__graphnodes', 'spaces__areas', 'spaces__areas__groups',
'spaces__pois', 'spaces__pois__groups')
levels = {}
spaces = {}
areas = {}
2017-11-28 22:40:01 +01:00
pois = {}
groups = {}
nodes = deque()
for level in levels_query:
buildings_geom = unary_union(tuple(building.geometry for building in level.buildings.all()))
nodes_before_count = len(nodes)
for group in level.groups.all():
groups.setdefault(group.pk, {}).setdefault('levels', set()).add(level.pk)
for space in level.spaces.all():
# create space geometries
accessible_geom = space.geometry.difference(unary_union(
tuple(column.geometry for column in space.columns.all()) +
tuple(hole.geometry for hole in space.holes.all()) +
((buildings_geom, ) if space.outside else ())
))
2017-11-28 22:02:24 +01:00
obstacles_geom = unary_union(
tuple(obstacle.geometry for obstacle in space.obstacles.all()) +
tuple(lineobstacle.buffered_geometry for lineobstacle in space.lineobstacles.all())
)
2017-11-28 22:40:01 +01:00
clear_geom = accessible_geom.difference(obstacles_geom)
clear_geom_prep = prepared.prep(clear_geom)
for group in space.groups.all():
groups.setdefault(group.pk, {}).setdefault('spaces', set()).add(space.pk)
2017-11-27 15:01:58 +01:00
space_nodes = tuple(RouterNode.from_graph_node(node, i)
for i, node in enumerate(space.graphnodes.all()))
for i, node in enumerate(space_nodes, start=len(nodes)):
node.i = i
nodes.extend(space_nodes)
for area in space.areas.all():
for group in area.groups.all():
groups.setdefault(group.pk, {}).setdefault('areas', set()).add(area.pk)
area._prefetched_objects_cache = {}
area = RouterArea(area)
area_nodes = tuple(node for node in space_nodes if area.geometry_prep.intersects(node.point))
area.nodes = set(node.i for node in area_nodes)
for node in area_nodes:
node.areas.add(area.pk)
areas[area.pk] = area
space._prefetched_objects_cache = {}
space = RouterSpace(space)
space.nodes = set(node.i for node in space_nodes)
for area in level.altitudeareas.all():
if not space.geometry_prep.intersects(area.geometry):
continue
2017-11-28 22:02:24 +01:00
for subgeom in assert_multipolygon(accessible_geom.intersection(area.geometry)):
area = RouterAltitudeArea(subgeom, subgeom.difference(obstacles_geom),
area.altitude, area.altitude2, area.point1, area.point2)
area_nodes = tuple(node for node in space_nodes if area.geometry_prep.intersects(node.point))
area.nodes = set(node.i for node in area_nodes)
for node in area_nodes:
altitude = area.get_altitude(node)
if node.altitude is None or node.altitude < altitude:
node.altitude = altitude
space.altitudeareas.append(area)
for area in space.altitudeareas:
# create fallback nodes
if not area.nodes and space_nodes:
fallback_point = good_representative_point(area.clear_geometry)
fallback_node = RouterNode(None, None, fallback_point.x, fallback_point.y,
space.pk, area.get_altitude(fallback_point))
# todo: check waytypes here
for node in space_nodes:
line = LineString([(node.x, node.y), (fallback_node.x, fallback_node.y)])
2017-11-28 22:40:01 +01:00
if not clear_geom_prep.crosses(line):
2017-11-28 22:02:24 +01:00
area.fallback_nodes[node.i] = (
fallback_node,
RouterEdge(fallback_node, node, 0)
)
if not area.fallback_nodes:
nearest_node = min(space_nodes, key=lambda node: fallback_point.distance(node.point))
area.fallback_nodes[nearest_node.i] = (
fallback_node,
RouterEdge(fallback_node, nearest_node, 0)
)
2017-11-28 22:40:01 +01:00
for poi in space.pois.all():
for group in poi.groups.all():
groups.setdefault(group.pk, {}).setdefault('pois', set()).add(poi.pk)
poi._prefetched_objects_cache = {}
poi = RouterPOI(poi)
altitudearea = space.altitudearea_for_point(poi.geometry)
poi_nodes = altitudearea.nodes_for_point(poi.geometry, all_nodes=nodes)
print(poi_nodes)
poi.nodes = set(i for i in poi_nodes.keys())
poi.nodes_addition = poi_nodes
pois[poi.pk] = poi
spaces[space.pk] = space
level_spaces = set(space.pk for space in level.spaces.all())
level._prefetched_objects_cache = {}
level = RouterLevel(level, spaces=level_spaces)
level.nodes = set(range(nodes_before_count, len(nodes)))
levels[level.pk] = level
# waytypes
waytypes = deque([RouterWayType(None)])
waytypes_lookup = {None: 0}
for i, waytype in enumerate(WayType.objects.all(), start=1):
waytypes.append(RouterWayType(waytype))
waytypes_lookup[waytype.pk] = i
waytypes = tuple(waytypes)
# collect nodes
nodes = tuple(nodes)
nodes_lookup = {node.pk: node.i for node in nodes}
# collect edges
edges = tuple(RouterEdge(from_node=nodes[nodes_lookup[edge.from_node_id]],
to_node=nodes[nodes_lookup[edge.to_node_id]],
waytype=waytypes_lookup[edge.waytype_id]) for edge in GraphEdge.objects.all())
2017-11-28 22:02:24 +01:00
edges = {(edge.from_node, edge.to_node): edge for edge in edges}
# build graph matrix
graph = np.full(shape=(len(nodes), len(nodes)), fill_value=np.inf, dtype=np.float32)
2017-11-27 15:01:58 +01:00
for edge in edges.values():
2017-11-28 22:02:24 +01:00
index = (edge.from_node, edge.to_node)
graph[index] = edge.distance
waytype = waytypes[edge.waytype]
(waytype.upwards_indices if edge.rise > 0 else waytype.nonupwards_indices).append(index)
# finalize waytype matrixes
for waytype in waytypes:
waytype.upwards_indices = np.array(waytype.upwards_indices, dtype=np.uint32).reshape((-1, 2))
waytype.nonupwards_indices = np.array(waytype.nonupwards_indices, dtype=np.uint32).reshape((-1, 2))
2017-11-28 22:40:01 +01:00
router = cls(levels, spaces, areas, pois, groups, nodes, edges, waytypes, graph)
2017-11-27 13:21:53 +01:00
pickle.dump(router, open(cls.filename, 'wb'))
return router
@classmethod
def load(cls):
return pickle.load(open(cls.filename, 'rb'))
def get_locations(self, location, permissions=frozenset()):
locations = ()
2017-11-27 16:46:03 +01:00
if isinstance(location, Level):
locations = (self.levels.get(location.pk), )
elif isinstance(location, Space):
locations = (self.spaces.get(location.pk), )
elif isinstance(location, Area):
locations = (self.areas.get(location.pk), )
2017-11-28 22:40:01 +01:00
elif isinstance(location, POI):
locations = (self.pois.get(location.pk), )
elif isinstance(location, LocationGroup):
group = self.groups.get(location.pk)
locations = tuple(chain(
(self.levels[pk] for pk in group.get('levels', ())),
(self.spaces[pk] for pk in group.get('spaces', ())),
(self.areas[pk] for pk in group.get('areas', ()))
))
2017-11-28 22:40:01 +01:00
# todo: route from/to custom location
return RouterLocation(tuple(location for location in locations
if location is not None and (location.access_restriction_id is None or
location.access_restriction_id in permissions)))
2017-11-27 15:01:58 +01:00
def get_route(self, origin, destination, permissions=frozenset()):
2017-11-27 15:01:58 +01:00
# get possible origins and destinations
origins = self.get_locations(origin, permissions=permissions)
destinations = self.get_locations(destination, permissions=permissions)
# todo: throw error if route is impossible
2017-11-27 15:01:58 +01:00
# calculate shortest path matrix
distances, predecessors = shortest_path(self.graph, directed=True, return_predecessors=True)
# find shortest path for our origins and destinations
origin_nodes = np.array(tuple(origins.nodes))
destination_nodes = np.array(tuple(destinations.nodes))
origin_node, destination_node = np.unravel_index(
distances[origin_nodes.reshape((-1, 1)), destination_nodes].argmin(),
(len(origin_nodes), len(destination_nodes))
)
2017-11-27 15:01:58 +01:00
origin_node = origin_nodes[origin_node]
destination_node = destination_nodes[destination_node]
# get best origin and destination
origin = origins.get_location_for_node(origin_node)
destination = destinations.get_location_for_node(destination_node)
# recreate path
path_nodes = deque((destination_node, ))
last_node = destination_node
while last_node != origin_node:
last_node = predecessors[origin_node, last_node]
path_nodes.appendleft(last_node)
path_nodes = tuple(path_nodes)
return Route(self, origin, destination, distances[origin_node, destination_node], path_nodes)
class BaseRouterProxy:
def __init__(self, src):
self.src = src
self.nodes = set()
2017-11-28 22:40:01 +01:00
self.nodes_addition = {}
@cached_property
def geometry_prep(self):
return prepared.prep(self.src.geometry)
def __getstate__(self):
result = self.__dict__.copy()
result.pop('geometry_prep', None)
return result
def __getattr__(self, name):
2017-11-27 13:41:16 +01:00
if name == '__setstate__':
raise AttributeError
return getattr(self.src, name)
class RouterLevel(BaseRouterProxy):
def __init__(self, level, spaces=None):
super().__init__(level)
self.spaces = spaces if spaces else set()
class RouterSpace(BaseRouterProxy):
def __init__(self, space, altitudeareas=None):
super().__init__(space)
self.altitudeareas = altitudeareas if altitudeareas else []
2017-11-28 22:40:01 +01:00
def altitudearea_for_point(self, point):
point = Point(point.x, point.y)
for area in self.altitudeareas:
if area.geometry_prep.intersects(point):
return area
return self.altitudareas[0]
class RouterArea(BaseRouterProxy):
pass
2017-11-28 22:40:01 +01:00
class RouterPOI(BaseRouterProxy):
pass
class RouterAltitudeArea:
2017-11-28 22:02:24 +01:00
def __init__(self, geometry, clear_geometry, altitude, altitude2, point1, point2):
self.geometry = geometry
2017-11-28 22:02:24 +01:00
self.clear_geometry = clear_geometry
self.altitude = altitude
self.altitude2 = altitude2
self.point1 = point1
self.point2 = point2
2017-11-28 22:02:24 +01:00
self.nodes = frozenset()
self.fallback_nodes = {}
@cached_property
def geometry_prep(self):
return prepared.prep(self.geometry)
2017-11-28 22:02:24 +01:00
@cached_property
def clear_geometry_prep(self):
return prepared.prep(self.clear_geometry)
def get_altitude(self, point):
# noinspection PyTypeChecker,PyCallByClass
return AltitudeArea.get_altitudes(self, (point.x, point.y))[0]
2017-11-28 22:40:01 +01:00
def nodes_for_point(self, point, all_nodes):
point = Point(point.x, point.y)
nodes = {}
if self.nodes:
for node in self.nodes:
node = all_nodes[node]
line = LineString([(node.x, node.y), (point.x, point.y)])
if not self.clear_geometry_prep.crosses(line):
nodes[node.i] = None
if not nodes:
nearest_node = min(tuple(all_nodes[node] for node in self.nodes),
key=lambda node: point.distance(node.point))
nodes[nearest_node.i] = None
else:
nodes = self.fallback_nodes
return nodes
def __getstate__(self):
result = self.__dict__.copy()
result.pop('geometry_prep', None)
2017-11-28 22:02:24 +01:00
result.pop('clear_geometry_prep', None)
return result
class RouterNode:
2017-11-27 15:01:58 +01:00
def __init__(self, i, pk, x, y, space, altitude=None, areas=None):
self.i = i
self.pk = pk
self.x = x
self.y = y
self.space = space
self.altitude = altitude
self.areas = areas if areas else set()
@classmethod
2017-11-27 15:01:58 +01:00
def from_graph_node(cls, node, i):
return cls(i, node.pk, node.geometry.x, node.geometry.y, node.space_id)
@cached_property
def point(self):
return Point(self.x, self.y)
@cached_property
def xyz(self):
return np.array((self.x, self.y, self.altitude))
class RouterEdge:
def __init__(self, from_node, to_node, waytype, rise=None, distance=None):
2017-11-28 22:02:24 +01:00
self.from_node = from_node.i
self.to_node = to_node.i
self.waytype = waytype
2017-11-28 22:02:24 +01:00
self.rise = rise if rise is not None else (to_node.altitude - from_node.altitude)
self.distance = distance if distance is not None else np.linalg.norm(to_node.xyz - from_node.xyz)
class RouterWayType:
def __init__(self, waytype):
2017-11-27 15:01:58 +01:00
self.src = waytype
self.upwards_indices = deque()
self.nonupwards_indices = deque()
2017-11-27 15:01:58 +01:00
def __getattr__(self, name):
if name == '__setstate__':
raise AttributeError
return getattr(self.src, name)
class RouterLocation:
def __init__(self, locations=()):
2017-11-28 22:40:01 +01:00
self.locations = tuple(location for location in locations if locations)
2017-11-27 15:01:58 +01:00
@cached_property
def nodes(self):
return reduce(operator.or_, (location.nodes for location in self.locations), frozenset())
def get_location_for_node(self, node):
for location in self.locations:
if node in location.nodes:
return location
return None