From 68773adcc2cb4e7b1dedc3222ce7539674d1c967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 3 Dec 2016 17:34:00 +0100 Subject: [PATCH] buildgraph: add doors and connections --- src/c3nav/routing/graph.py | 70 ++++++++++++++++++++++++++++++++++---- src/c3nav/routing/utils.py | 51 +++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/src/c3nav/routing/graph.py b/src/c3nav/routing/graph.py index ecb300fc..e5cf938e 100644 --- a/src/c3nav/routing/graph.py +++ b/src/c3nav/routing/graph.py @@ -1,4 +1,5 @@ import os +from itertools import permutations from django.conf import settings from django.utils.functional import cached_property @@ -6,7 +7,7 @@ from PIL import Image, ImageDraw from shapely.geometry import JOIN_STYLE, LineString, Polygon from c3nav.mapdata.models import Level -from c3nav.routing.utils import get_coords_angles, polygon_to_mpl_path +from c3nav.routing.utils import assert_multipolygon, get_coords_angles, get_nearest_point, polygon_to_mpl_path class GraphLevel(): @@ -29,12 +30,35 @@ class GraphLevel(): for room in self.rooms: room.create_points() + doors = self.level.geometries.doors + doors = assert_multipolygon(doors) + for door in doors: + polygon = door.buffer(0.01, join_style=JOIN_STYLE.mitre) + center = door.centroid + points = [] + for room in self.rooms: + if polygon.intersects(room.geometry): + nearest_point = get_nearest_point(room.clear_geometry, center) + point = GraphPoint(room, *nearest_point.coords[0]) + points.append(point) + room.points.append(point) + + if len(points) < 2: + print('waaaah') + + for from_point, to_point in permutations(points, 2): + from_point.connect_to(to_point) + def _ellipse_bbox(self, x, y, height): x *= settings.RENDER_SCALE y *= settings.RENDER_SCALE y = height-y return ((x - 2, y - 2), (x + 2, y + 2)) + def _line_coords(self, from_point, to_point, height): + return (from_point.x * settings.RENDER_SCALE, height - (from_point.y * settings.RENDER_SCALE), + to_point.x * settings.RENDER_SCALE, height - (to_point.y * settings.RENDER_SCALE)) + def draw_png(self): filename = os.path.join(settings.RENDER_ROOT, 'level-%s.png' % self.level.name) graph_filename = os.path.join(settings.RENDER_ROOT, 'level-%s-graph.png' % self.level.name) @@ -47,6 +71,8 @@ class GraphLevel(): for point in room.points: i += 1 draw.ellipse(self._ellipse_bbox(point.x, point.y, height), (255, 0, 0)) + for otherpoint, connection in point.connections.items(): + draw.line(self._line_coords(point, otherpoint, height), fill=(255, 0, 0)) print(i, 'points') im.save(graph_filename) @@ -69,17 +95,30 @@ class GraphRoom(): if geometry.is_empty: return - if isinstance(geometry, Polygon): - polygons = [geometry] - else: - polygons = geometry.geoms - + # points with 60cm distance to borders + polygons = assert_multipolygon(geometry) for polygon in polygons: self._add_ring(polygon.exterior, want_left=False) for interior in polygon.interiors: self._add_ring(interior, want_left=True) + # now fill in missing doorways or similar + missing_geometry = self.clear_geometry.difference(geometry.buffer(0.61, join_style=JOIN_STYLE.mitre)) + polygons = assert_multipolygon(missing_geometry) + for polygon in polygons: + overlaps = polygon.buffer(0.62).intersection(geometry) + if overlaps.is_empty: + continue + overlaps = assert_multipolygon(overlaps) + for overlap in overlaps: + self.points.append(GraphPoint(self, *overlap.centroid.coords[0])) + + self._add_ring(polygon.exterior, want_left=False) + + for interior in polygon.interiors: + self._add_ring(interior, want_left=True) + def _add_ring(self, geom, want_left): """ add the points of a ring, but only those that have a specific direction change. @@ -117,6 +156,8 @@ class GraphPoint(): self.room = room self.x = x self.y = y + self.connections = {} + self.connections_in = {} @cached_property def ellipse_bbox(self): @@ -124,10 +165,24 @@ class GraphPoint(): y = self.y * settings.RENDER_SCALE return ((x-5, y-5), (x+5, y+5)) + def connect_to(self, to_point): + self.room.level.graph.add_connection(self, to_point) + + +class GraphConnection(): + def __init__(self, graph, from_point, to_point): + if to_point in from_point.connections: + raise ValueError('already connected') + + self.graph = graph + from_point.connections[to_point] = self + to_point.connections_in[from_point] = self + class Graph(): def __init__(self): self.levels = {} + self.connections = [] def build(self): for level in Level.objects.all(): @@ -136,3 +191,6 @@ class Graph(): for level in self.levels.values(): level.build() level.draw_png() + + def add_connection(self, from_point, to_point): + self.connections.append(GraphConnection(self, from_point, to_point)) diff --git a/src/c3nav/routing/utils.py b/src/c3nav/routing/utils.py index 362ecc43..7f578125 100644 --- a/src/c3nav/routing/utils.py +++ b/src/c3nav/routing/utils.py @@ -2,6 +2,7 @@ from math import atan2, degrees import numpy as np from matplotlib.path import Path +from shapely.geometry import Polygon def cleanup_coords(coords): @@ -72,3 +73,53 @@ def _mpl_add_linearring(linearring, vertices, codes): vertices.append(coords[0]) codes.append(Path.MOVETO) codes.extend([Path.LINETO] * len(coords)) + + +def assert_multipolygon(geometry): + """ + given a Polygon or a MultiPolygon, return a list of Polygons + :param geometry: a Polygon or a MultiPolygon + :return: a list of Polygons + """ + if isinstance(geometry, Polygon): + polygons = [geometry] + else: + polygons = geometry.geoms + return polygons + + +def get_nearest_point(polygon, point): + """ + calculate the nearest point on of a polygon to another point that lies outside + :param polygon: a Polygon or a MultiPolygon + :param point: something that shapely understands as a point + :return: a Point + """ + polygons = assert_multipolygon(polygon) + nearest_distance = float('inf') + nearest_point = None + for polygon in polygons: + if point.within(Polygon(polygon.exterior.coords)): + for interior in polygon.interiors: + if point.within(Polygon(interior.coords)): + point_ = _nearest_point_ring(interior, point) + distance = point_.distance(point) + if distance and distance < nearest_distance: + nearest_distance = distance + nearest_point = point_ + break # in a valid polygon a point can not be within multiple interiors + break # in a valid multipolygon a point can not be within multiple polygons + else: + point_ = _nearest_point_ring(polygon.exterior, point) + distance = point_.distance(point) + if distance and distance < nearest_distance: + nearest_distance = distance + nearest_point = point_ + + if nearest_point is None: + raise ValueError('Point inside polygon.') + return nearest_point + + +def _nearest_point_ring(ring, point): + return ring.interpolate(ring.project(point))