diff --git a/src/c3nav/routing/area.py b/src/c3nav/routing/area.py index a5a60836..c74a0693 100644 --- a/src/c3nav/routing/area.py +++ b/src/c3nav/routing/area.py @@ -7,17 +7,27 @@ from c3nav.routing.utils.coords import coord_angle class GraphArea(): - def __init__(self, room, mpl_clear, mpl_stairs): + def __init__(self, room, mpl_clear, mpl_stairs, points=None): self.room = room self.graph = room.graph self.mpl_clear = mpl_clear self.mpl_stairs = mpl_stairs - self.points = [] + self.points = points + + def serialize(self): + return ( + self.mpl_clear, + self.mpl_stairs, + self.points, + ) + + def prepare_build(self): + self._built_points = [] def build_connections(self): - for point1, point2 in combinations(self.points, 2): + for point1, point2 in combinations(self._built_points, 2): path = Path(np.vstack((point1.xy, point2.xy))) # lies within room @@ -40,11 +50,14 @@ class GraphArea(): if not valid: continue - self.graph.add_connection(point1, point2) - self.graph.add_connection(point2, point1) + point1.connect_to(point2) + point2.connect_to(point1) def add_point(self, point): if not self.mpl_clear.contains_point(point.xy): return False - self.points.append(point) + self._built_points.append(point) return True + + def finish_build(self): + self.points = np.array(tuple(point.i for point in self._built_points)) diff --git a/src/c3nav/routing/connection.py b/src/c3nav/routing/connection.py index 01afcf38..b784ebb7 100644 --- a/src/c3nav/routing/connection.py +++ b/src/c3nav/routing/connection.py @@ -2,14 +2,10 @@ import numpy as np class GraphConnection(): - def __init__(self, graph, from_point, to_point, distance=None): - self.graph = graph + def __init__(self, from_point, to_point, distance=None): self.from_point = from_point self.to_point = to_point self.distance = distance if distance is not None else abs(np.linalg.norm(from_point.xy - to_point.xy)) - if to_point in from_point.connections: - self.graph.connections.remove(from_point.connections[to_point]) - - from_point.connections[to_point] = self - to_point.connections_in[from_point] = self + def serialize(self): + return (self.distance, ) diff --git a/src/c3nav/routing/graph.py b/src/c3nav/routing/graph.py index 17add9b9..01dc713f 100644 --- a/src/c3nav/routing/graph.py +++ b/src/c3nav/routing/graph.py @@ -2,14 +2,13 @@ import os import pickle from collections import OrderedDict +import numpy as np from django.conf import settings from c3nav.mapdata.models import Level from c3nav.mapdata.models.geometry import LevelConnector -from c3nav.routing.connection import GraphConnection from c3nav.routing.level import GraphLevel from c3nav.routing.point import GraphPoint -from c3nav.routing.room import GraphRoom class Graph: @@ -20,60 +19,61 @@ class Graph: for level in Level.objects.all(): self.levels[level.name] = GraphLevel(self, level) - self.rooms = [] self.points = [] - self.connections = [] - - self.level_transfer_points = [] - self.levelconnector_points = {} + self.level_transfer_points = None # Building the Graph def build(self): + self._built_level_transfer_points = [] + self._built_levelconnector_points = {} + for level in self.levels.values(): level.build() # collect rooms and points - self.rooms = sum((level.rooms for level in self.levels.values()), []) - self.points = sum((level.points for level in self.levels.values()), []) + rooms = sum((level.rooms for level in self.levels.values()), []) + self.points = sum((level._built_points for level in self.levels.values()), []) # create connections between levels print() self.connect_levelconnectors() - # convert everything to tuples - self.rooms = tuple(self.rooms) + # finishing build: creating numpy arrays and convert everything else to tuples self.points = tuple(self.points) - self.connections = tuple(self.connections) - # give numbers to rooms and points - for i, room in enumerate(self.rooms): + for i, room in enumerate(rooms): room.i = i for i, point in enumerate(self.points): point.i = i + self.level_transfer_points = np.array(tuple(point.i for point in self._built_level_transfer_points)) + + for level in self.levels.values(): + level.finish_build() + print() print('Total:') - print('%d points' % len(self.points)) - print('%d rooms' % len(self.rooms)) - print('%d level transfer points' % len(self.level_transfer_points)) - print('%d connections' % len(self.connections)) + self.print_stats() print() print('Points per room:') for name, level in self.levels.items(): print(('Level %s:' % name), *(sorted((len(room.points) for room in level.rooms), reverse=True))) - def add_connection(self, from_point, to_point, distance=None): - self.connections.append(GraphConnection(self, from_point, to_point, distance)) + def print_stats(self): + print('%d points' % len(self.points)) + print('%d rooms' % sum(len(level.rooms) for level in self.levels.values())) + print('%d level transfer points' % len(self.level_transfer_points)) + print('%d connections' % sum(len(point.connections) for point in self.points)) def add_levelconnector_point(self, levelconnector, point): - self.levelconnector_points.setdefault(levelconnector.name, []).append(point) + self._built_levelconnector_points.setdefault(levelconnector.name, []).append(point) def connect_levelconnectors(self): for levelconnector in LevelConnector.objects.all(): center = levelconnector.geometry.centroid - points = self.levelconnector_points.get(levelconnector.name, []) + points = self._built_levelconnector_points.get(levelconnector.name, []) rooms = tuple(set(sum((point.rooms for point in points), []))) if len(rooms) < 2: @@ -84,26 +84,27 @@ class Graph: center_point = GraphPoint(center.x, center.y, rooms=rooms) self.points.append(center_point) + self._built_level_transfer_points.append(center_point) levels = tuple(set(room.level for room in rooms)) for level in levels: - level.room_transfer_points.append(center_point) - level.points.append(center_point) + level._built_room_transfer_points.append(center_point) + level._built_points.append(center_point) for room in rooms: - room.points.append(center_point) + room._built_points.append(center_point) for point in points: - self.add_connection(center_point, point) - self.add_connection(point, center_point) + center_point.connect_to(point) + point.connect_to(center_point) # Loading/Saving the Graph def serialize(self): - rooms = tuple((room.level.level.name, room.mpl_clear) for room in self.rooms) - points = tuple((point.x, point.y, tuple(room.i for room in point.rooms)) for point in self.points) - connections = tuple((conn.from_point.i, conn.to_point.i, conn.distance) for conn in self.connections) - - return rooms, points, connections + return ( + {name: level.serialize() for name, level in self.levels.items()}, + [point.serialize() for point in self.points], + self.level_transfer_points, + ) def save(self, filename=None): if filename is None: @@ -113,24 +114,14 @@ class Graph: @classmethod def unserialize(cls, data): + levels, points, level_transfer_points = data + graph = cls() - rooms, points, connections = data - - graph.rooms = [GraphRoom(graph.levels[room[0]], mpl_clear=room[1]) for room in rooms] - graph.points = [GraphPoint(point[0], point[1], rooms=tuple(graph.rooms[i] for i in point[2])) - for point in points] - - for point in graph.points: - for room in point.rooms: - room.points.append(point) - for name, level in graph.levels.items(): - level.rooms = [room for room in graph.rooms if room.level == level] - level.points = list(set(sum((room.points for room in level.rooms), []))) - level.room_transfer_points = [point for point in level.points if len(point.rooms) > 1] + level.unserialize(levels[name]) - for from_point, to_point, distance in connections: - graph.add_connection(graph.points[from_point], graph.points[to_point], distance) + graph.points = tuple(GraphPoint(*point) for point in points) + graph.level_transfer_points = level_transfer_points return graph @@ -140,6 +131,7 @@ class Graph: filename = cls.default_filename with open(filename, 'rb') as f: graph = cls.unserialize(pickle.load(f)) + graph.print_stats() return graph # Drawing diff --git a/src/c3nav/routing/level.py b/src/c3nav/routing/level.py index 388307fd..85437783 100644 --- a/src/c3nav/routing/level.py +++ b/src/c3nav/routing/level.py @@ -1,5 +1,6 @@ import os +import numpy as np from django.conf import settings from PIL import Image, ImageDraw from shapely.geometry import JOIN_STYLE @@ -15,15 +16,32 @@ class GraphLevel(): def __init__(self, graph, level): self.graph = graph self.level = level + self.rooms = [] self.points = [] - self.room_transfer_points = [] - self.rooms = [] + self.room_transfer_points = None + self.level_transfer_points = None + + def serialize(self): + return ( + [room.serialize() for room in self.rooms], + self.points, + self.room_transfer_points, + self.level_transfer_points, + ) + + def unserialize(self, data): + rooms, self.points, self.room_transfer_points, self.level_transfer_points = data + self.rooms = tuple(GraphRoom.unserialize(self, room) for room in rooms) # Building the Graph def build(self): print() print('Level %s:' % self.level.name) + + self._built_points = [] + self._built_room_transfer_points = [] + self.collect_rooms() print('%d rooms' % len(self.rooms)) @@ -34,21 +52,21 @@ class GraphLevel(): self.create_doors() self.create_levelconnectors() - self.points = sum((room.points for room in self.rooms), []) - self.points.extend(self.room_transfer_points) + self._built_points = sum((room._built_points for room in self.rooms), []) + self._built_points.extend(self._built_room_transfer_points) for room in self.rooms: room.build_connections() - print('%d points' % len(self.points)) - print('%d room transfer points' % len(self.room_transfer_points)) + print('%d points' % len(self._built_points)) + print('%d room transfer points' % len(self._built_room_transfer_points)) def collect_rooms(self): accessibles = self.level.geometries.accessible accessibles = assert_multipolygon(accessibles) for geometry in accessibles: - room = GraphRoom(self, geometry) - if room.prepare_build(): + room = GraphRoom(self) + if room.prepare_build(geometry): self.rooms.append(room) def create_doors(self): @@ -62,14 +80,13 @@ class GraphLevel(): connected_rooms = set() points = [] for room in self.rooms: - if not polygon.intersects(room.geometry): + if not polygon.intersects(room._built_geometry): continue - for subpolygon in assert_multipolygon(polygon.intersection(room.geometry)): + for subpolygon in assert_multipolygon(polygon.intersection(room._built_geometry)): connected_rooms.add(room) nearest_point = get_nearest_point(room.clear_geometry, subpolygon.centroid) point, = room.add_point(nearest_point.coords[0]) - room.points.append(point) points.append(point) if len(points) < 2: @@ -77,30 +94,39 @@ class GraphLevel(): continue center_point = GraphPoint(center.x, center.y, rooms=tuple(connected_rooms)) - self.room_transfer_points.append(center_point) + self._built_room_transfer_points.append(center_point) for room in connected_rooms: - room.points.append(center_point) + room._built_points.append(center_point) for point in points: - self.graph.add_connection(center_point, point) - self.graph.add_connection(point, center_point) + center_point.connect_to(point) + point.connect_to(center_point) def create_levelconnectors(self): for levelconnector in self.level.levelconnectors.all(): polygon = levelconnector.geometry for room in self.rooms: - if not polygon.intersects(room.geometry): + if not polygon.intersects(room._built_geometry): continue - for subpolygon in assert_multipolygon(polygon.intersection(room.geometry)): + for subpolygon in assert_multipolygon(polygon.intersection(room._built_geometry)): point = subpolygon.centroid if not point.within(room.clear_geometry): point = get_nearest_point(room.clear_geometry, point) point, = room.add_point(point.coords[0]) - room.points.append(point) + room._built_points.append(point) self.graph.add_levelconnector_point(levelconnector, point) + def finish_build(self): + self.rooms = tuple(self.rooms) + self.points = np.array(tuple(point.i for point in self._built_points)) + self.room_transfer_points = np.array(tuple(point.i for point in self._built_room_transfer_points)) + self.level_transfer_points = np.array(tuple(i for i in self.points if i in self.graph.level_transfer_points)) + + for room in self.rooms: + room.finish_build() + # Drawing def draw_png(self, points=True, lines=True): filename = os.path.join(settings.RENDER_ROOT, 'level-%s.base.png' % self.level.name) @@ -118,10 +144,10 @@ class GraphLevel(): for point in self.points: draw.ellipse(_ellipse_bbox(point.x, point.y, height), (200, 0, 0)) - for point in self.room_transfer_points: + for point in self._built_room_transfer_points: draw.ellipse(_ellipse_bbox(point.x, point.y, height), (0, 0, 255)) - for point in self.room_transfer_points: + for point in self._built_room_transfer_points: for otherpoint, connection in point.connections.items(): draw.line(_line_coords(point, otherpoint, height), fill=(0, 255, 255)) diff --git a/src/c3nav/routing/point.py b/src/c3nav/routing/point.py index cb8ad441..35124438 100644 --- a/src/c3nav/routing/point.py +++ b/src/c3nav/routing/point.py @@ -2,6 +2,8 @@ import numpy as np from django.conf import settings from django.utils.functional import cached_property +from c3nav.routing.connection import GraphConnection + class GraphPoint(): def __init__(self, x, y, room=None, rooms=None): @@ -13,8 +15,19 @@ class GraphPoint(): self.connections = {} self.connections_in = {} + def serialize(self): + return ( + self.x, + self.y, + ) + @cached_property def ellipse_bbox(self): x = self.x * settings.RENDER_SCALE y = self.y * settings.RENDER_SCALE return ((x-5, y-5), (x+5, y+5)) + + def connect_to(self, other_point): + connection = GraphConnection(self, other_point) + self.connections[other_point] = connection + other_point.connections_in[self] = connection diff --git a/src/c3nav/routing/room.py b/src/c3nav/routing/room.py index 844ea7b2..de7d9adf 100644 --- a/src/c3nav/routing/room.py +++ b/src/c3nav/routing/room.py @@ -11,23 +11,41 @@ from c3nav.routing.utils.mpl import shapely_to_mpl class GraphRoom(): - def __init__(self, level, geometry=None, mpl_clear=None): + def __init__(self, level): self.level = level self.graph = level.graph - self.geometry = geometry - self.mpl_clear = mpl_clear + self.mpl_clear = None self.areas = [] - self.points = [] + self.points = None + self.room_transfer_points = None + + def serialize(self): + return ( + self.mpl_clear, + [area.serialize() for area in self.areas], + self.points, + self.room_transfer_points, + ) + + @classmethod + def unserialize(cls, level, data): + room = cls(level) + room.mpl_clear, areas, room.points, room.room_transfer_points = data + room.areas = tuple(GraphArea(room, *area) for area in areas) + return room # Building the Graph - def prepare_build(self): - self.clear_geometry = self.geometry.buffer(-0.3, join_style=JOIN_STYLE.mitre) + def prepare_build(self, geometry): + self._built_geometry = geometry + self.clear_geometry = self._built_geometry.buffer(-0.3, join_style=JOIN_STYLE.mitre) if self.clear_geometry.is_empty: return False + self._built_points = [] + self.mpl_clear = shapely_to_mpl(self.clear_geometry.buffer(0.01, join_style=JOIN_STYLE.mitre)) self.mpl_stairs = () for stair_line in assert_multilinestring(self.level.level.geometries.stairs): @@ -40,7 +58,7 @@ class GraphRoom(): def build_areas(self): stairs_areas = self.level.level.geometries.stairs stairs_areas = stairs_areas.buffer(0.3, join_style=JOIN_STYLE.mitre, cap_style=CAP_STYLE.flat) - stairs_areas = stairs_areas.intersection(self.geometry) + stairs_areas = stairs_areas.intersection(self._built_geometry) self.stairs_areas = assert_multipolygon(stairs_areas) isolated_areas = tuple(assert_multipolygon(stairs_areas.intersection(self.clear_geometry))) @@ -50,10 +68,12 @@ class GraphRoom(): mpl_clear = shapely_to_mpl(isolated_area.buffer(0.01, join_style=JOIN_STYLE.mitre)) mpl_stairs = tuple((stair, angle) for stair, angle in self.mpl_stairs if mpl_clear.intersects_path(stair, filled=True)) - self.areas.append(GraphArea(self, mpl_clear, mpl_stairs)) + area = GraphArea(self, mpl_clear, mpl_stairs) + area.prepare_build() + self.areas.append(area) def build_points(self): - narrowed_geometry = self.geometry.buffer(-0.6, join_style=JOIN_STYLE.mitre) + narrowed_geometry = self._built_geometry.buffer(-0.6, join_style=JOIN_STYLE.mitre) geometry = narrowed_geometry.buffer(0.31, join_style=JOIN_STYLE.mitre).intersection(self.clear_geometry) if geometry.is_empty: @@ -150,7 +170,7 @@ class GraphRoom(): if not self.mpl_clear.contains_point(coord): return [] point = GraphPoint(coord[0], coord[1], self) - self.points.append(point) + self._built_points.append(point) for area in self.areas: area.add_point(point) return [point] @@ -159,7 +179,15 @@ class GraphRoom(): for area in self.areas: area.build_connections() + def finish_build(self): + self.areas = tuple(self.areas) + self.points = np.array(tuple(point.i for point in self._built_points)) + self.room_transfer_points = np.array(tuple(i for i in self.points if i in self.level.room_transfer_points)) + + for area in self.areas: + area.finish_build() + # Routing def build_router(self): self.router = Router() - self.router.build(self.points) + self.router.build(self._built_points)