diff --git a/src/c3nav/mapdata/render/data.py b/src/c3nav/mapdata/render/data.py index dcc14fdb..5771fd03 100644 --- a/src/c3nav/mapdata/render/data.py +++ b/src/c3nav/mapdata/render/data.py @@ -1,11 +1,29 @@ import pickle +from itertools import chain +import numpy as np from django.core.cache import cache from django.db import transaction +from shapely.geometry import LineString, MultiLineString from shapely.ops import unary_union from c3nav.mapdata.cache import MapHistory from c3nav.mapdata.models import Level, MapUpdate +from c3nav.mapdata.utils.geometry import get_rings +from c3nav.mapdata.utils.mesh import triangulate_rings +from c3nav.mapdata.utils.mpl import shapely_to_mpl + + +class HybridGeometry: + def __init__(self, geom, faces): + self._geom = geom + self._faces = faces + + @classmethod + def create(cls, geom, face_centers): + if isinstance(geom, (LineString, MultiLineString)): + return HybridGeometry(geom, set()) + return HybridGeometry(geom, set(np.argwhere(shapely_to_mpl(geom).contains_points(face_centers)).flatten())) class AltitudeAreaGeometries: @@ -18,6 +36,14 @@ class AltitudeAreaGeometries: self.altitude = None self.colors = colors + def get_geometries(self): + return chain((self.geometry, ), chain(*(areas.values() for areas in self.colors.values()))) + + def create_hybrid_geometries(self, face_centers): + self.geometry = HybridGeometry.create(self.geometry, face_centers) + self.colors = {color: {key: HybridGeometry.create(geom, face_centers) for key, geom in areas.items()} + for color, areas in self.colors.items()} + class FakeCropper: @staticmethod @@ -129,6 +155,8 @@ class LevelRenderData: if not new_area.is_empty: new_geoms.restricted_spaces_outdoors[access_restriction] = new_area + new_geoms.build_mesh() + render_data.levels.append((new_geoms, sublevel.default_height)) render_data.access_restriction_affected = { @@ -156,6 +184,9 @@ class LevelGeometries: self.restricted_spaces_outdoors = None self.affected_area = None + self.vertices = None + self.faces = None + @staticmethod def build_for_level(level): geoms = LevelGeometries() @@ -236,8 +267,28 @@ class LevelGeometries: for access_restriction, spaces in restricted_spaces_outdoors.items()} geoms.walls = buildings_geom.difference(spaces_geom).difference(doors_geom) + return geoms + def get_geometries(self): + return chain(chain(*(area.get_geometries() for area in self.altitudeareas)), (self.walls, self.doors, ), + self.restricted_spaces_indoors.values(), self.restricted_spaces_outdoors.values()) + + def create_hybrid_geometries(self, face_centers): + for area in self.altitudeareas: + area.create_hybrid_geometries(face_centers) + self.walls = HybridGeometry.create(self.walls, face_centers) + self.doors = HybridGeometry.create(self.doors, face_centers) + self.restricted_spaces_indoors = {key: HybridGeometry.create(geom, face_centers) + for key, geom in self.restricted_spaces_indoors.items()} + self.restricted_spaces_outdoors = {key: HybridGeometry.create(geom, face_centers) + for key, geom in self.restricted_spaces_outdoors.items()} + + def build_mesh(self): + rings = tuple(chain(*(get_rings(geom) for geom in self.get_geometries()))) + self.vertices, self.faces = triangulate_rings(rings) + self.create_hybrid_geometries(face_centers=self.vertices[self.faces].sum(axis=1)/3) + def get_level_render_data(level): cache_key = 'mapdata:level_render_data:%s:%s' % (str(level.pk if isinstance(level, Level) else level), diff --git a/src/c3nav/mapdata/utils/geometry.py b/src/c3nav/mapdata/utils/geometry.py index e33a8c9d..48717a0a 100644 --- a/src/c3nav/mapdata/utils/geometry.py +++ b/src/c3nav/mapdata/utils/geometry.py @@ -5,7 +5,7 @@ from django.core import checks from matplotlib.patches import PathPatch from matplotlib.path import Path from shapely import speedups -from shapely.geometry import LineString, Polygon +from shapely.geometry import LinearRing, LineString, Polygon if speedups.available: speedups.enable() @@ -91,3 +91,19 @@ def plot_geometry(geom, title=None, bounds=None): patch = PathPatch(path) axes.add_patch(patch) plt.show() + + +def get_rings(geometry): + if isinstance(geometry, Polygon): + return chain((geometry.exterior, ), geometry.interiors) + try: + geoms = geometry.geoms + except AttributeError: + pass + else: + return chain(*(get_rings(geom) for geom in geoms)) + + if isinstance(geometry, LinearRing): + return (geometry, ) + + return () diff --git a/src/c3nav/mapdata/utils/mesh.py b/src/c3nav/mapdata/utils/mesh.py index cd48f58c..72ecef9e 100644 --- a/src/c3nav/mapdata/utils/mesh.py +++ b/src/c3nav/mapdata/utils/mesh.py @@ -14,6 +14,31 @@ def get_face_indizes(start, length): return np.vstack((indices, (indices[-1][-1], indices[0][0]))) +def triangulate_rings(rings, holes=None): + rings = tuple(tuple(tuple(vertex) for vertex in (np.array(ring.coords)*1000).astype(np.uint64)) for ring in rings) + + if not rings: + return np.empty((0, 2), dtype=np.float32), np.empty((0, 3), dtype=np.int32) + + vertices = tuple(set(chain(*rings))) + vertices_lookup = {vertex: i for i, vertex in enumerate(vertices)} + + segments = set() + for ring in rings: + indices = tuple(vertices_lookup[vertex] for vertex in ring[:-1]) + segments.update(zip(indices, indices[1:]+indices[:1])) + + info = triangle.MeshInfo() + info.set_points(np.array(vertices).tolist()) + info.set_facets(segments) + + if holes is not None: + info.set_holes(holes) + + mesh = triangle.build(info, quality_meshing=False) + return np.array(mesh.points)/1000, np.array(mesh.elements) + + def _triangulate_polygon(polygon: Polygon, keep_holes=False): vertices = deque() segments = deque()