diff --git a/src/c3nav/routing/graph.py b/src/c3nav/routing/graph.py index 2121f017..49adfbdc 100644 --- a/src/c3nav/routing/graph.py +++ b/src/c3nav/routing/graph.py @@ -12,6 +12,7 @@ from c3nav.mapdata.models.geometry import LevelConnector from c3nav.mapdata.models.locations import AreaLocation, Location, LocationGroup, PointLocation from c3nav.routing.level import GraphLevel from c3nav.routing.point import GraphPoint +from c3nav.routing.route import RoomRouteSegment, Route, NoRoute, LevelRouteSegment, GraphRouteSegment class Graph: @@ -215,7 +216,7 @@ class Graph: def _get_points_by_i(self, points): return tuple(self.points[i] for i in points) - def _get_index_of_allowed_points(self, points, allowed_points_i): + def _allowed_points_index(self, points, allowed_points_i): return np.array(tuple(i for i, point in enumerate(points) if point in allowed_points_i)) def get_route(self, origin: Location, destination: Location): @@ -225,8 +226,7 @@ class Graph: orig_points = self._get_points_by_i(orig_points_i) dest_points = self._get_points_by_i(dest_points_i) - best_route_distance = float('inf') - best_route = None + best_route = NoRoute # get routers graph_router, level_routers, room_routers = self.build_routers() @@ -237,8 +237,8 @@ class Graph: common_rooms = orig_rooms & dest_rooms # get origin points for each room (points as point index within room) - orig_room_points = {room: self._get_index_of_allowed_points(room.points, orig_points_i) for room in orig_rooms} - dest_room_points = {room: self._get_index_of_allowed_points(room.points, dest_points_i) for room in dest_rooms} + orig_room_points = {room: self._allowed_points_index(room.points, orig_points_i) for room in orig_rooms} + dest_room_points = {room: self._allowed_points_index(room.points, dest_points_i) for room in dest_rooms} # if the points have common rooms, search for routes within those rooms if common_rooms: @@ -248,15 +248,17 @@ class Graph: distance = shortest_paths.min() # Is this route better than the previous ones? - if distance >= best_route_distance: + if distance >= best_route.distance: continue # noinspection PyTypeChecker - best_route = ('room', room, np.argwhere(shortest_paths == distance)[0]) - best_route_distance = distance + from_point, to_point = np.argwhere(shortest_paths == distance)[0] + from_point = orig_room_points[from_point] + to_point = dest_room_points[to_point] + best_route = RoomRouteSegment(room, room_routers[room], from_point, to_point).as_route() # get reachable room transfer points and their distance - # as a dictionary: global transfer point index => (global index of closest location point, distance) + # as a dictionary: global transfer point index => RoomRouteSegment orig_room_transfers = self._room_transfers(orig_rooms, orig_room_points, room_routers, mode='orig') dest_room_transfers = self._room_transfers(dest_rooms, dest_room_points, room_routers, mode='dest') @@ -266,29 +268,68 @@ class Graph: common_levels = orig_levels & dest_levels # get reachable roomtransfer points for each level (points as room transfer point index within level) - orig_level_points = {level: self._get_index_of_allowed_points(level.room_transfer_points, orig_room_transfers) - for level in orig_levels} - dest_level_points = {level: self._get_index_of_allowed_points(level.room_transfer_points, dest_room_transfers) - for level in dest_levels} + orig_room_transfer_points = {level: self._allowed_points_index(level.room_transfer_points, orig_room_transfers) + for level in orig_levels} + dest_room_transfer_points = {level: self._allowed_points_index(level.room_transfer_points, dest_room_transfers) + for level in dest_levels} + # if the points have common rooms, search for routes within thos levels if common_levels: for level in common_levels: - o_points = orig_level_points[level] - d_points = dest_level_points[level] + o_points = orig_room_transfer_points[level] + d_points = dest_room_transfer_points[level] shortest_paths = level_routers[level].shortest_paths[o_points[:, None], d_points] - shortest_paths += np.array(tuple(orig_room_transfers[level.room_transfer_points[in_level_i]][1] + + # add distances to the the room transfer points to the rows and columns + shortest_paths += np.array(tuple(orig_room_transfers[level.room_transfer_points[in_level_i]].distance for in_level_i in o_points))[:, None] - shortest_paths += np.array(tuple(dest_room_transfers[level.room_transfer_points[in_level_i]][1] + shortest_paths += np.array(tuple(dest_room_transfers[level.room_transfer_points[in_level_i]].distance for in_level_i in d_points)) distance = shortest_paths.min() # Is this route better than the previous ones? - if distance >= best_route_distance: + if distance >= best_route.distance: continue # noinspection PyTypeChecker - best_route = ('level', level, np.argwhere(shortest_paths == distance)[0]) - best_route_distance = distance + from_point, to_point = np.argwhere(shortest_paths == distance)[0] + from_point = o_points[from_point] + to_point = d_points[to_point] + best_route = Route((orig_room_transfers[level.room_transfer_points[from_point]], + LevelRouteSegment(level, level_routers[level], from_point, to_point), + dest_room_transfers[level.room_transfer_points[to_point]]), distance=distance) + + # get reachable level transfer points and their distance + # as a dictionary: global transfer point index => Route + orig_level_transfers = self._level_transfers(orig_levels, orig_room_transfers, level_routers, mode='orig') + dest_level_transfers = self._level_transfers(orig_levels, dest_room_transfers, level_routers, mode='dest') + + # get reachable roomtransfer points for each level (points as room transfer point index within level) + orig_level_transfer_points = self._allowed_points_index(self.level_transfer_points, orig_level_transfers) + dest_level_transfer_points = self._allowed_points_index(self.level_transfer_points, dest_level_transfers) + + # search a route within the whole graph + if True: + o_points = orig_level_transfer_points + d_points = dest_level_transfer_points + shortest_paths = graph_router.shortest_paths[o_points[:, None], d_points] + + # add distances to the the room transfer points to the rows and columns + shortest_paths += np.array(tuple(orig_level_transfers[self.level_transfer_points[in_graph_i]].distance + for in_graph_i in o_points))[:, None] + shortest_paths += np.array(tuple(dest_level_transfers[self.level_transfer_points[in_graph_i]].distance + for in_graph_i in d_points)) + distance = shortest_paths.min() + + # Is this route better than the previous ones? + if distance < best_route.distance: + # noinspection PyTypeChecker + from_point, to_point = np.argwhere(shortest_paths == distance)[0] + from_point = o_points[from_point] + to_point = d_points[to_point] + best_route = Route((orig_level_transfers[self.level_transfer_points[from_point]], + GraphRouteSegment(self, graph_router, from_point, to_point), + dest_level_transfers[self.level_transfer_points[to_point]]), distance=distance) return best_route @@ -309,13 +350,54 @@ class Graph: # noinspection PyTypeChecker for from_i, to_i in np.argwhere(shortest_paths != np.inf): distance = shortest_paths[from_i, to_i] - location_i, transfer_i = (from_i, to_i) if mode == 'orig' else (to_i, from_i) - location_i = room.points[points[location_i]] - transfer_i = room.points[room_transfer_points[transfer_i]] - if transfer_i not in room_transfers or room_transfers[transfer_i][1] < distance: - room_transfers[transfer_i] = (location_i, distance) + from_i = points[from_i] if mode == 'orig' else room_transfer_points[from_i] + to_i = room_transfer_points[to_i] if mode == 'orig' else points[to_i] + + transfer_i = room.points[to_i if mode == 'orig' else from_i] + if transfer_i not in room_transfers or room_transfers[transfer_i].distance < distance: + room_transfers[transfer_i] = RoomRouteSegment(room, room_routers[room], from_i, to_i) return room_transfers + def _level_transfers(self, levels, all_room_transfers, level_routers, mode): + if mode not in ('orig', 'dest'): + raise ValueError + + level_transfers = {} + for level in levels: + level_transfer_points = np.array(tuple(level.room_transfer_points.index(point) + for point in level.level_transfer_points)) + + points, distances = zip(*(tuple((level.room_transfer_points.index(point), segment.distance) + for point, segment in all_room_transfers.items() + if point in level.room_transfer_points))) + points = np.array(points) + distances = np.array(distances) + + if mode == 'orig': + shortest_paths = level_routers[level].shortest_paths[points[:, None], level_transfer_points] + shortest_paths += distances[:, None] + else: + shortest_paths = level_routers[level].shortest_paths[level_transfer_points[:, None], points] + shortest_paths += distances + + # noinspection PyTypeChecker + for from_i, to_i in np.argwhere(shortest_paths != np.inf): + distance = shortest_paths[from_i, to_i] + from_i = points[from_i] if mode == 'orig' else level_transfer_points[from_i] + to_i = level_transfer_points[to_i] if mode == 'orig' else points[to_i] + + transfer_i = level.room_transfer_points[to_i if mode == 'orig' else from_i] + room_transfer_i = level.room_transfer_points[from_i if mode == 'orig' else to_i] + if transfer_i not in level_transfers or level_transfers[transfer_i].distance < distance: + segments = [LevelRouteSegment(level, level_routers[level], from_i, to_i)] + if mode == 'orig': + segments.insert(0, all_room_transfers[room_transfer_i]) + else: + segments.append(all_room_transfers[room_transfer_i]) + level_transfers[transfer_i] = Route(segments, distance) + + return level_transfers + GraphRouter = namedtuple('GraphRouter', ('shortest_paths', 'predecessors', )) diff --git a/src/c3nav/routing/route.py b/src/c3nav/routing/route.py new file mode 100644 index 00000000..3409d687 --- /dev/null +++ b/src/c3nav/routing/route.py @@ -0,0 +1,88 @@ +from abc import ABC + +import numpy as np +from django.utils.functional import cached_property + + +class RouteSegment(ABC): + def __init__(self, router, from_point, to_point): + """ + :param router: a Router (RoomRouter, GraphRouter, …) + :param from_point: in-router index of first point + :param to_point: in-router index of last point + """ + self.router = router + self.from_point = int(from_point) + self.to_point = int(to_point) + + def as_route(self): + return Route([self]) + + @cached_property + def distance(self): + return self.router.shortest_paths[self.from_point, self.to_point] + + +class RoomRouteSegment(RouteSegment): + def __init__(self, room, router, from_point, to_point): + """ + Route segment within a Room + :param room: GraphRoom + :param router: RoomRouter + :param from_point: in-room index of first point + :param to_point: in-room index of last point + """ + super().__init__(router, from_point, to_point) + self.room = room + self.global_from_point = room.points[from_point] + self.global_to_point = room.points[to_point] + + def __repr__(self): + return ('' % + (self.room, self.from_point, self.to_point, self.distance)) + + +class LevelRouteSegment(RouteSegment): + def __init__(self, level, router, from_point, to_point): + """ + Route segment within a Level (from room transfer point to room transfer point) + :param level: GraphLevel + """ + super().__init__(router, from_point, to_point) + self.level = level + self.global_from_point = level.room_transfer_points[from_point] + self.global_to_point = level.room_transfer_points[to_point] + + def __repr__(self): + return ('' % + (self.level, self.from_point, self.to_point, self.distance)) + + +class GraphRouteSegment(RouteSegment): + def __init__(self, graph, router, from_point, to_point): + """ + Route segment within a Graph (from level transfer point to level transfer point) + :param graph: Graph + """ + super().__init__(router, from_point, to_point) + self.graph = graph + self.global_from_point = graph.level_transfer_points[from_point] + self.global_to_point = graph.level_transfer_points[to_point] + + def __repr__(self): + return ('' % + (self.graph, self.from_point, self.to_point, self.distance)) + + +class Route: + def __init__(self, segments, distance=None): + self.segments = sum(((item.segments if isinstance(item, Route) else (item, )) for item in segments), ()) + self.distance = sum(segment.distance for segment in self.segments) + + def __repr__(self): + return ('' % + ('\n '.join(repr(segment) for segment in self.segments), self.distance)) + + +class NoRoute: + distance = np.inf