diff --git a/src/c3nav/routing/graph.py b/src/c3nav/routing/graph.py index e030f349..ecb300fc 100644 --- a/src/c3nav/routing/graph.py +++ b/src/c3nav/routing/graph.py @@ -1,17 +1,12 @@ import os -from math import atan2, pi, degrees -from PIL import Image -from PIL import ImageDraw from django.conf import settings -from kombu.utils import cached_property -from shapely.geometry import JOIN_STYLE -from shapely.geometry import LineString -from shapely.geometry import MultiPolygon -from shapely.geometry import Point -from shapely.geometry import Polygon +from django.utils.functional import cached_property +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 class GraphLevel(): @@ -63,38 +58,9 @@ class GraphRoom(): self.geometry = geometry self.points = [] - def cleanup_coords(self, coords): - result = [] - last_coord = coords[-1] - for coord in coords: - if ((coord[0] - last_coord[0]) ** 2 + (coord[1] - last_coord[1]) ** 2) ** 0.5 >= 0.01: - result.append(coord) - last_coord = coord - return result + self.clear_geometry = geometry.buffer(-0.3, join_style=JOIN_STYLE.mitre) - def coord_angle(self, coord1, coord2): - return degrees(atan2(-(coord2[1] - coord1[1]), coord2[0] - coord1[0])) % 360 - - def split_coords_by_angle(self, geom): - coords = list(self.cleanup_coords(geom.coords)) - last_coords = coords[-2:] - last_angle = self.coord_angle(last_coords[-2], last_coords[-1]) - left = [] - right = [] - for coord in coords: - angle = self.coord_angle(last_coords[-1], coord) - angle_diff = (last_angle-angle) % 360 - if angle_diff < 180: - left.append(last_coords[-1]) - else: - right.append(last_coords[-1]) - last_coords.append(coord) - last_angle = angle - - if not geom.is_ccw: - left, right = right, left - - return left, right + self.mpl_path = polygon_to_mpl_path(geometry) def create_points(self): original_geometry = self.geometry @@ -109,14 +75,41 @@ class GraphRoom(): polygons = geometry.geoms for polygon in polygons: - left, right = self.split_coords_by_angle(polygon.exterior) - for x, y in right: - self.points.append(GraphPoint(self, x, y)) + self._add_ring(polygon.exterior, want_left=False) for interior in polygon.interiors: - left, right = self.split_coords_by_angle(interior) - for x, y in left: - self.points.append(GraphPoint(self, x, y)) + 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. + additionally removes unneeded points if the neighbors can be connected in self.clear_geometry + :param geom: LinearRing + :param want_left: True if the direction has to be left, False if it has to be right + """ + coords = [] + skipped = False + can_delete_last = False + for coord, is_left in get_coords_angles(geom): + if is_left != want_left: + skipped = True + continue + + if not skipped and can_delete_last and len(coords) >= 2: + if LineString((coords[-2], coord)).within(self.clear_geometry): + coords[-1] = coord + continue + + coords.append(coord) + can_delete_last = not skipped + skipped = False + + if not skipped and can_delete_last and len(coords) >= 3: + if LineString((coords[-2], coords[0])).within(self.clear_geometry): + coords.pop() + + for coord in coords: + self.points.append(GraphPoint(self, *coord)) class GraphPoint(): @@ -143,5 +136,3 @@ class Graph(): for level in self.levels.values(): level.build() level.draw_png() - - diff --git a/src/c3nav/routing/management/commands/buildgraph.py b/src/c3nav/routing/management/commands/buildgraph.py index 120589a7..d49b822b 100644 --- a/src/c3nav/routing/management/commands/buildgraph.py +++ b/src/c3nav/routing/management/commands/buildgraph.py @@ -1,6 +1,5 @@ from django.core.management.base import BaseCommand -from c3nav.mapdata.render import render_all_levels from c3nav.routing.graph import Graph diff --git a/src/c3nav/routing/utils.py b/src/c3nav/routing/utils.py new file mode 100644 index 00000000..362ecc43 --- /dev/null +++ b/src/c3nav/routing/utils.py @@ -0,0 +1,74 @@ +from math import atan2, degrees + +import numpy as np +from matplotlib.path import Path + + +def cleanup_coords(coords): + """ + remove coordinates that are closer than 0.01 (1cm) + :param coords: list of (x, y) coordinates + :return: list of (x, y) coordinates + """ + result = [] + last_coord = coords[-1] + for coord in coords: + if ((coord[0] - last_coord[0]) ** 2 + (coord[1] - last_coord[1]) ** 2) ** 0.5 >= 0.01: + result.append(coord) + last_coord = coord + return result + + +def coord_angle(coord1, coord2): + """ + calculate angle in degrees from coord1 to coord2 + :param coord1: (x, y) coordinate + :param coord2: (x, y) coordinate + :return: angle in degrees + """ + return degrees(atan2(-(coord2[1] - coord1[1]), coord2[0] - coord1[0])) % 360 + + +def get_coords_angles(geom): + """ + inspects all coordinates of a LinearRing counterclockwise and checks if they are a left or a right turn. + :param geom: LinearRing + :rtype: a list of ((x, y), is_left) tuples + """ + coords = list(cleanup_coords(geom.coords)) + last_coords = coords[-2:] + last_angle = coord_angle(last_coords[-2], last_coords[-1]) + result = [] + + invert = not geom.is_ccw + + for coord in coords: + angle = coord_angle(last_coords[-1], coord) + angle_diff = (last_angle-angle) % 360 + result.append((last_coords[-1], (angle_diff < 180) ^ invert)) + last_coords.append(coord) + last_angle = angle + + return result + + +def polygon_to_mpl_path(polygon): + """ + convert a shapely Polygon to a matplotlib Path + :param polygon: shapely Polygon + :return: matplotlib Path + """ + vertices = [] + codes = [] + _mpl_add_linearring(polygon.exterior, vertices, codes) + for interior in polygon.interiors: + _mpl_add_linearring(interior, vertices, codes) + return Path(np.array(vertices), codes=codes) + + +def _mpl_add_linearring(linearring, vertices, codes): + coords = list(linearring.coords) + vertices.extend(coords) + vertices.append(coords[0]) + codes.append(Path.MOVETO) + codes.extend([Path.LINETO] * len(coords))