diff --git a/src/c3nav/routing/graph/graph.py b/src/c3nav/routing/graph/graph.py index 21fb1ecb..7d0095b3 100644 --- a/src/c3nav/routing/graph/graph.py +++ b/src/c3nav/routing/graph/graph.py @@ -10,6 +10,7 @@ from c3nav.routing.graph.connection import GraphConnection from c3nav.routing.graph.level import GraphLevel from c3nav.routing.graph.point import GraphPoint from c3nav.routing.graph.room import GraphRoom +from c3nav.routing.graph.router import Router class Graph(): @@ -25,6 +26,9 @@ class Graph(): self.rooms = [] self.levelconnector_points = {} + self.transfer_points = [] + self.router = Router() + def build(self): for level in self.levels.values(): level.build() @@ -88,9 +92,16 @@ class Graph(): graph = cls.unserialize(pickle.load(f)) return graph - def draw_pngs(self, points=True, lines=True): + def build_router(self): + for room in self.rooms: + room.build_router() + self.transfer_points.extend(room.router.transfer_points) + + self.router.build(self.transfer_points, global_routing=True) + + def draw_pngs(self, points=True, lines=True, transfer_points=False, transfer_lines=False): for level in self.levels.values(): - level.draw_png(points=points, lines=lines) + level.draw_png(points, lines, transfer_points, transfer_lines) def add_levelconnector_point(self, levelconnector, point): self.levelconnector_points.setdefault(levelconnector.name, []).append(point) diff --git a/src/c3nav/routing/graph/level.py b/src/c3nav/routing/graph/level.py index 8429dd77..18e85576 100644 --- a/src/c3nav/routing/graph/level.py +++ b/src/c3nav/routing/graph/level.py @@ -78,7 +78,7 @@ class GraphLevel(): print('%d points' % len(self.points)) print() - def draw_png(self, points=True, lines=True): + def draw_png(self, points=True, lines=True, transfer_points=False, transfer_lines=False): filename = os.path.join(settings.RENDER_ROOT, 'level-%s.base.png' % self.level.name) graph_filename = os.path.join(settings.RENDER_ROOT, 'level-%s.graph.png' % self.level.name) @@ -94,4 +94,15 @@ class GraphLevel(): for point in self.points: draw.ellipse(_ellipse_bbox(point.x, point.y, height), (200, 0, 0)) + if transfer_lines: + for point in self.points: + if point.in_room_transfer_distances is not None: + for otherpoint, distance in point.in_room_transfer_distances.items(): + draw.line(_line_coords(point, otherpoint, height), fill=(100, 100, 255)) + + if transfer_points: + for point in self.points: + if point.in_room_transfer_distances is not None: + draw.ellipse(_ellipse_bbox(point.x, point.y, height), (0, 0, 200)) + im.save(graph_filename) diff --git a/src/c3nav/routing/graph/point.py b/src/c3nav/routing/graph/point.py index e4fa6d21..61df89fe 100644 --- a/src/c3nav/routing/graph/point.py +++ b/src/c3nav/routing/graph/point.py @@ -10,6 +10,7 @@ class GraphPoint(): self.xy = (x, y) self.connections = {} self.connections_in = {} + self.in_room_transfer_distances = None @cached_property def ellipse_bbox(self): diff --git a/src/c3nav/routing/graph/room.py b/src/c3nav/routing/graph/room.py index c704f6f8..a519f34b 100644 --- a/src/c3nav/routing/graph/room.py +++ b/src/c3nav/routing/graph/room.py @@ -6,6 +6,7 @@ from shapely.geometry import JOIN_STYLE, LineString from c3nav.mapdata.utils import assert_multipolygon from c3nav.routing.graph.point import GraphPoint +from c3nav.routing.graph.router import Router from c3nav.routing.utils.coords import get_coords_angles from c3nav.routing.utils.mpl import polygon_to_mpl_paths @@ -19,6 +20,8 @@ class GraphRoom(): self.clear_geometry = geometry.buffer(-0.3, join_style=JOIN_STYLE.mitre) self.empty = self.clear_geometry.is_empty + self.router = Router() + if mpl_paths is not None: self.mpl_paths = mpl_paths elif not self.empty: @@ -63,6 +66,10 @@ class GraphRoom(): for from_point, to_point in permutations(points, 2): from_point.connect_to(to_point) + # noinspection PyTypeChecker + def build_router(self): + self.router.build(self.points) + def _add_ring(self, geom, want_left): """ add the points of a ring, but only those that have a specific direction change. diff --git a/src/c3nav/routing/graph/router.py b/src/c3nav/routing/graph/router.py new file mode 100644 index 00000000..e4678d23 --- /dev/null +++ b/src/c3nav/routing/graph/router.py @@ -0,0 +1,52 @@ +import numpy as np +from scipy.sparse import csr_matrix +from scipy.sparse.csgraph import shortest_path + + +class Router(): + def __init__(self): + self.points = [] + self.points_pk = None + + self.transfer_points = set() + self.shortest_paths = None + self.predecessors = None + + self._built = False + + # noinspection PyTypeChecker + def build(self, points, global_routing=False): + if self._built: + raise RuntimeError('already built.') + self._built = True + + self.points = points + self.points_pk = dict(zip(self.points, range(len(self.points)))) + matrix = np.zeros((len(self.points), len(self.points))) + + for point, pk in self.points_pk.items(): + for to_point, connection in point.connections.items(): + if to_point not in self.points_pk: + if not global_routing: + self.transfer_points.add(point) + continue + matrix[pk, self.points_pk[to_point]] = 1 + if global_routing: + for to_point, distance in point.in_room_transfer_distances.items(): + matrix[pk, self.points_pk[to_point]] = distance + + g_sparse = csr_matrix(np.ma.masked_values(np.fromstring(matrix).reshape(matrix.shape), 0)) + self.shortest_paths, self.predecessors = shortest_path(g_sparse, return_predecessors=True) + + if not global_routing: + for from_point in self.transfer_points: + from_point.in_room_transfer_distances = {} + connections = self.shortest_paths[self.points_pk[from_point], ] + for to_point_pk in np.argwhere(connections != np.inf).flatten(): + to_point = self.points[to_point_pk] + if to_point not in self.transfer_points: + continue + from_point.in_room_transfer_distances[to_point] = connections[to_point_pk] + + def get_distance(self, from_point, to_point): + return self.shortest_paths[self.points_pk[from_point], self.points_pk[from_point]] diff --git a/src/c3nav/routing/management/commands/drawgraph.py b/src/c3nav/routing/management/commands/drawgraph.py index eef67bf1..07cb599d 100644 --- a/src/c3nav/routing/management/commands/drawgraph.py +++ b/src/c3nav/routing/management/commands/drawgraph.py @@ -13,6 +13,15 @@ class Command(BaseCommand): parser.add_argument('--no-lines', action='store_const', dest='lines', const=False, default=True, help='dont draw lines on the graph image') + parser.add_argument('--transfer-points', action='store_const', const=True, default=False, + help='highlight transfer points') + + parser.add_argument('--transfer-lines', action='store_const', const=True, default=False, + help='draw in-room transfer lines') + def handle(self, *args, **options): graph = Graph.load() - graph.draw_pngs(points=options['points'], lines=options['lines']) + if options['transfer_points'] or options['transfer_lines']: + graph.build_router() + graph.draw_pngs(points=options['points'], lines=options['lines'], + transfer_points=options['transfer_points'], transfer_lines=options['transfer_lines']) diff --git a/src/c3nav/routing/management/commands/testgraph.py b/src/c3nav/routing/management/commands/testgraph.py new file mode 100644 index 00000000..795a68a7 --- /dev/null +++ b/src/c3nav/routing/management/commands/testgraph.py @@ -0,0 +1,19 @@ +import time + +from django.core.management.base import BaseCommand + +from c3nav.routing.graph import Graph + + +class Command(BaseCommand): + help = 'check how long it takes to build the routers for the routing graph' + + def handle(self, *args, **options): + start = time.time() + graph = Graph.load() + print('Graph loaded in %.4fs' % (time.time() - start)) + + start = time.time() + graph.build_router() + print('Routers built in %.4fs' % (time.time() - start)) + print('%s transfer points' % len(graph.transfer_points)) diff --git a/src/requirements/production.txt b/src/requirements/production.txt index 414057fb..5e296e04 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -9,3 +9,4 @@ celery>=3.1,<3.2 requests>=2.11,<2.12 Pillow>=3.4.2,<3.5 matplotlib>=1.5.3,<1.6 +scipy>=0.18.1,<0.19