From 9f264d1f59377c97045a8b98717bc2b80c342b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Mon, 19 Aug 2024 15:19:12 +0200 Subject: [PATCH] make LevelRenderData into a dataclass --- .../mapdata/render/geometry/altitudearea.py | 4 +- src/c3nav/mapdata/render/geometry/level.py | 262 ++++++++++-------- src/c3nav/mapdata/render/renderdata.py | 102 ++++--- 3 files changed, 215 insertions(+), 153 deletions(-) diff --git a/src/c3nav/mapdata/render/geometry/altitudearea.py b/src/c3nav/mapdata/render/geometry/altitudearea.py index b2939dc3..f717991e 100644 --- a/src/c3nav/mapdata/render/geometry/altitudearea.py +++ b/src/c3nav/mapdata/render/geometry/altitudearea.py @@ -26,11 +26,11 @@ class AltitudeAreaGeometries: self.obstacles = obstacles @property - def min_altitude(self): + def min_altitude(self) -> float: return self.altitude if self.altitude is not None else min(p.altitude for p in self.points) @property - def max_altitude(self): + def max_altitude(self) -> float: return self.altitude if self.altitude is not None else max(p.altitude for p in self.points) def get_altitudes(self, points): diff --git a/src/c3nav/mapdata/render/geometry/level.py b/src/c3nav/mapdata/render/geometry/level.py index b4c12cae..3e48e58f 100644 --- a/src/c3nav/mapdata/render/geometry/level.py +++ b/src/c3nav/mapdata/render/geometry/level.py @@ -6,12 +6,11 @@ from functools import reduce from itertools import chain import numpy as np -from django.db.backends.ddl_references import Columns from shapely import prepared from shapely.geometry import GeometryCollection, Polygon, MultiPolygon from shapely.ops import unary_union -from c3nav.mapdata.models import Area, Space +from c3nav.mapdata.models import Space, Level, AltitudeArea from c3nav.mapdata.render.geometry.altitudearea import AltitudeAreaGeometries from c3nav.mapdata.render.geometry.hybrid import HybridGeometry from c3nav.mapdata.render.geometry.mesh import Mesh @@ -28,45 +27,46 @@ empty_geometry_collection = GeometryCollection() ZeroOrMorePolygons: typing.TypeAlias = GeometryCollection | Polygon | MultiPolygon +@dataclass class LevelGeometries: """ Store geometries for a Level. """ - def __init__(self): - self.buildings = None - self.altitudeareas = [] - self.heightareas = [] - self.walls = None - self.walls_extended = None - self.all_walls = None - self.short_walls = [] - self.doors = None - self.doors_extended = None - self.holes = None - self.access_restriction_affected = None - self.restricted_spaces_indoors = None - self.restricted_spaces_outdoors = None - self.affected_area = None - self.ramps = [] + # todo: split into the two versions of this + buildings: ZeroOrMorePolygons + altitudeareas: list[AltitudeAreaGeometries] + heightareas: typing.Sequence[tuple[ZeroOrMorePolygons, float]] + walls: ZeroOrMorePolygons + walls_extended: None | HybridGeometry + all_walls: ZeroOrMorePolygons + short_walls: list[tuple[AltitudeArea, ZeroOrMorePolygons]] | typing.Sequence[ZeroOrMorePolygons] + doors: ZeroOrMorePolygons | None + doors_extended: HybridGeometry | None + holes: None + access_restriction_affected: dict[int, ZeroOrMorePolygons] | None + restricted_spaces_indoors: dict[int, ZeroOrMorePolygons] + restricted_spaces_outdoors: dict[int, ZeroOrMorePolygons] + affected_area: ZeroOrMorePolygons | None + ramps: typing.Sequence[ZeroOrMorePolygons] - self.vertices = None - self.faces = None + vertices: None | np.ndarray + faces: None | np.ndarray - self.walls_base = None - self.walls_bottom = None + walls_base: None | HybridGeometry + walls_bottom: None | HybridGeometry - self.pk = None - self.on_top_of_id = None - self.short_label = None - self.base_altitude = None - self.default_height = None - self.door_height = None - self.min_altitude = None - self.max_altitude = None - self.max_height = None + pk: int + on_top_of_id: int | None + short_label: str + base_altitude: int + default_height: int + door_height: int + min_altitude: int + max_altitude: int + max_height: int - self.lower_bound = None - self.upper_bound = None + lower_bound: int + upper_bound: None def __repr__(self): return '' % (self.short_label, self.pk) @@ -80,9 +80,9 @@ class LevelGeometries: instance: Space @classmethod - def spaces_for_level(cls, level, buildings_geom) -> list[SpaceGeometries]: + def spaces_for_level(cls, level: Level, buildings_geom: ZeroOrMorePolygons) -> list[SpaceGeometries]: spaces: list[cls.SpaceGeometries] = [] - for space in level.spaces.all(): + for space in level.spaces.all(): # noqa geometry = space.geometry subtract = [] if space.outside: @@ -124,7 +124,7 @@ class LevelGeometries: ramps: list[ZeroOrMorePolygons] @classmethod - def analyze_spaces(cls, level, spaces: list[SpaceGeometries], walkable_spaces_geom: ZeroOrMorePolygons, + def analyze_spaces(cls, level: Level, spaces: list[SpaceGeometries], walkable_spaces_geom: ZeroOrMorePolygons, buildings_geom: ZeroOrMorePolygons, color_manager: 'ThemeColorManager') -> Analysis: buildings_geom_prep = prepared.prep(buildings_geom) @@ -145,7 +145,7 @@ class LevelGeometries: for space in spaces: buffered = space.geometry.buffer(0.01).union(unary_union(tuple( unwrap_geom(door.geometry) - for door in level.doors.all() if door.geometry.intersects(unwrap_geom(space.geometry)) + for door in level.doors.all() if door.geometry.intersects(unwrap_geom(space.geometry)) # noqa )).difference(walkable_spaces_geom)) intersects = buildings_geom_prep.intersects(buffered) @@ -236,36 +236,12 @@ class LevelGeometries: ramps=ramps, ) - + @classmethod - def build_for_level(cls, level, color_manager: 'ThemeColorManager', altitudeareas_above): - geoms = LevelGeometries() - buildings_geom = unary_union([unwrap_geom(b.geometry) for b in level.buildings.all()]) - - # remove columns and holes from space areas - spaces = cls.spaces_for_level(level, buildings_geom) - - spaces_geom = unary_union([unwrap_geom(space.geometry) for space in spaces]) - doors_geom = unary_union([unwrap_geom(d.geometry) for d in level.doors.all()]) - doors_geom = doors_geom.intersection(buildings_geom) - walkable_spaces_geom = unary_union([unwrap_geom(space.walkable_geom) for space in spaces]) - doors_geom = doors_geom.difference(walkable_spaces_geom) - if level.on_top_of_id is None: - geoms.holes = unary_union([s.holes_geom for s in spaces]) - - analysis = cls.analyze_spaces( - level=level, - spaces=spaces, - walkable_spaces_geom=walkable_spaces_geom, - buildings_geom=buildings_geom, - color_manager=color_manager - ) - - geoms.buildings = buildings_geom - geoms.doors = doors_geom - + def build_altitudeareas(cls, level: Level, analysis: Analysis) -> list[AltitudeAreaGeometries]: # add altitudegroup geometries and split ground colors into them - for altitudearea in level.altitudeareas.all(): + altitudearea_geoms: list[AltitudeAreaGeometries] = [] + for altitudearea in level.altitudeareas.all(): # noqa altitudearea_prep = prepared.prep(unwrap_geom(altitudearea.geometry)) altitudearea_colors = {color: {access_restriction: area.intersection(unwrap_geom(altitudearea.geometry)) for access_restriction, area in areas.items() @@ -286,59 +262,121 @@ class LevelGeometries: if new_height_obstacles: altitudearea_obstacles[height] = new_height_obstacles - geoms.altitudeareas.append(AltitudeAreaGeometries(altitudearea, - altitudearea_colors, - altitudearea_obstacles)) - - # merge height areas - geoms.heightareas = tuple((unary_union(geoms), height) - for height, geoms in sorted(analysis.heightareas.items(), key=operator.itemgetter(0))) - - # merge access restrictions - geoms.access_restriction_affected = { - access_restriction: unary_union([unwrap_geom(geom) for geom in areas]) - for access_restriction, areas in analysis.access_restriction_affected.items() - } - geoms.restricted_spaces_indoors = { - access_restriction: unary_union(spaces) - for access_restriction, spaces in analysis.restricted_spaces_indoors.items() - } - geoms.restricted_spaces_outdoors = { - access_restriction: unary_union(spaces) - for access_restriction, spaces in analysis.restricted_spaces_outdoors.items() - } - - AccessRestrictionAffected.build(geoms.access_restriction_affected).save_level(level.pk, 'base') - - geoms.walls = buildings_geom.difference(unary_union((spaces_geom, doors_geom))) - - # shorten walls if there are altitudeareas above - remaining = geoms.walls + altitudearea_geoms.append(AltitudeAreaGeometries( + altitudearea=altitudearea, + colors=altitudearea_colors, + obstacles=altitudearea_obstacles + )) + return altitudearea_geoms + + @classmethod + def build_short_walls(cls, altitudeareas_above, + walls_geom: ZeroOrMorePolygons) -> list[tuple[AltitudeArea, ZeroOrMorePolygons]]: + remaining = walls_geom + short_walls = [] for altitudearea in altitudeareas_above: intersection = altitudearea.geometry.intersection(remaining).buffer(0) if intersection.is_empty: continue remaining = remaining.difference(unwrap_geom(altitudearea.geometry)) - geoms.short_walls.append((altitudearea, intersection)) - geoms.all_walls = geoms.walls - geoms.walls = geoms.walls.difference( - unary_union(tuple(unwrap_geom(altitudearea.geometry) for altitudearea in altitudeareas_above)) + short_walls.append((altitudearea, intersection)) + return short_walls + + @classmethod + def build_for_level(cls, level: Level, color_manager: 'ThemeColorManager', altitudeareas_above): + buildings_geom = unary_union([unwrap_geom(b.geometry) for b in level.buildings.all()]) # noqa + + # remove columns and holes from space areas + spaces = cls.spaces_for_level(level, buildings_geom) + + spaces_geom = unary_union([unwrap_geom(space.geometry) for space in spaces]) + doors_geom = unary_union([unwrap_geom(d.geometry) for d in level.doors.all()]) # noqa + doors_geom = doors_geom.intersection(buildings_geom) + walkable_spaces_geom = unary_union([unwrap_geom(space.walkable_geom) for space in spaces]) + doors_geom = doors_geom.difference(walkable_spaces_geom) + walls_geom = buildings_geom.difference(unary_union((spaces_geom, doors_geom))) + if level.on_top_of_id is None: + holes_geom = unary_union([s.holes_geom for s in spaces]) + else: + holes_geom = None + + analysis = cls.analyze_spaces( + level=level, + spaces=spaces, + walkable_spaces_geom=walkable_spaces_geom, + buildings_geom=buildings_geom, + color_manager=color_manager ) - # general level infos - geoms.pk = level.pk - geoms.on_top_of_id = level.on_top_of_id - geoms.short_label = level.short_label - geoms.base_altitude = int(level.base_altitude * 1000) - geoms.default_height = int(level.default_height * 1000) - geoms.door_height = int(level.door_height * 1000) - geoms.min_altitude = (min(area.min_altitude for area in geoms.altitudeareas) - if geoms.altitudeareas else geoms.base_altitude) - geoms.max_altitude = (max(area.max_altitude for area in geoms.altitudeareas) - if geoms.altitudeareas else geoms.base_altitude) - geoms.max_height = (min(height for area, height in geoms.heightareas) - if geoms.heightareas else geoms.default_height) - geoms.lower_bound = geoms.min_altitude-700 + altitudearea_geoms = cls.build_altitudeareas(level=level, analysis=analysis) + heightareas_geom = tuple((unary_union(geoms), height) for height, geoms in + sorted(analysis.heightareas.items(), key=operator.itemgetter(0))) + + base_altitude = int(level.base_altitude * 1000) + default_height = int(level.default_height * 1000) + door_height = int(level.door_height * 1000) + + min_altitude = (min(area.min_altitude for area in altitudearea_geoms) if altitudearea_geoms else base_altitude) + + # hybrid geometries + + geoms = cls( + ramps=analysis.ramps, + + buildings=buildings_geom, + doors=doors_geom, + holes=holes_geom, + + altitudeareas=altitudearea_geoms, + heightareas=heightareas_geom, + + # merge access restrictions + access_restriction_affected={ + access_restriction: unary_union([unwrap_geom(geom) for geom in areas]) + for access_restriction, areas in analysis.access_restriction_affected.items() + }, + restricted_spaces_indoors={ + access_restriction: unary_union(spaces) + for access_restriction, spaces in analysis.restricted_spaces_indoors.items() + }, + restricted_spaces_outdoors={ + access_restriction: unary_union(spaces) + for access_restriction, spaces in analysis.restricted_spaces_outdoors.items() + }, + + # shorten walls if there are altitudeareas above + short_walls=cls.build_short_walls(altitudeareas_above, walls_geom), + + all_walls=walls_geom, + walls=walls_geom.difference( + unary_union(tuple(unwrap_geom(altitudearea.geometry) for altitudearea in altitudeareas_above)) + ), + + # general level infos + pk=level.pk, + on_top_of_id=level.on_top_of_id, + short_label=level.short_label, + base_altitude=base_altitude, + default_height=default_height, + door_height=door_height, + min_altitude=min_altitude, + max_altitude=(max(area.max_altitude for area in altitudearea_geoms) + if altitudearea_geoms else base_altitude), + max_height=(min(height for area, height in heightareas_geom) + if analysis.heightareas else default_height), + lower_bound=min_altitude-700, + + affected_area=None, + doors_extended=None, + faces=None, + upper_bound=None, + vertices=None, + walls_base=None, + walls_bottom=None, + walls_extended=None, + ) + + AccessRestrictionAffected.build(geoms.access_restriction_affected).save_level(level.pk, 'base') return geoms @@ -388,7 +426,7 @@ class LevelGeometries: vertex_values = np.empty(self.vertices.shape[:1], dtype=np.int32) if not vertex_values.size: return vertex_values - vertex_value_mask = np.full(self.vertices.shape[:1], fill_value=False, dtype=np.bool) + vertex_value_mask = np.full(self.vertices.shape[:1], fill_value=False, dtype=bool) for item in items: faces = area_func(item).faces diff --git a/src/c3nav/mapdata/render/renderdata.py b/src/c3nav/mapdata/render/renderdata.py index b60c7c1e..4e3fb17e 100644 --- a/src/c3nav/mapdata/render/renderdata.py +++ b/src/c3nav/mapdata/render/renderdata.py @@ -206,18 +206,20 @@ class LevelRenderData: elif render_level.pk != level.pk: map_history.composite(MapHistory.open_level(level.pk, 'base'), None) - new_geoms = LevelGeometries() - new_geoms.buildings = crop_to.intersection(old_geoms.buildings) + new_buildings_geoms = crop_to.intersection(old_geoms.buildings) if old_geoms.on_top_of_id is None: - new_geoms.holes = crop_to.intersection(old_geoms.holes) - new_geoms.doors = crop_to.intersection(old_geoms.doors) - new_geoms.walls = crop_to.intersection(old_geoms.walls) - new_geoms.all_walls = crop_to.intersection(old_geoms.all_walls) - new_geoms.short_walls = tuple((altitude, geom) for altitude, geom in tuple( + new_holes_geoms = crop_to.intersection(old_geoms.holes) + else: + new_holes_geoms = None + new_doors_geoms = crop_to.intersection(old_geoms.doors) + new_walls_geoms = crop_to.intersection(old_geoms.walls) + new_all_walls_geoms = crop_to.intersection(old_geoms.all_walls) + new_short_walls_geoms = tuple((altitude, geom) for altitude, geom in tuple( (altitude, crop_to.intersection(geom)) for altitude, geom in old_geoms.short_walls ) if not geom.is_empty) + new_altitudeareas = [] for altitudearea in old_geoms.altitudeareas: new_geometry = crop_to.intersection(unwrap_geom(altitudearea.geometry)) if new_geometry.is_empty: @@ -258,59 +260,81 @@ class LevelRenderData: new_altitudearea_obstacles[height] = new_height_obstacles new_altitudearea.obstacles = new_altitudearea_obstacles - new_geoms.altitudeareas.append(new_altitudearea) + new_altitudeareas.append(new_altitudearea) - if new_geoms.walls.is_empty and not new_geoms.altitudeareas: + if new_walls_geoms.is_empty and not new_altitudeareas: continue - new_geoms.ramps = tuple( - ramp for ramp in (crop_to.intersection(unwrap_geom(ramp)) for ramp in old_geoms.ramps) - if not ramp.is_empty - ) - - new_geoms.heightareas = tuple( + new_heightareas = tuple( (area, height) for area, height in ((crop_to.intersection(unwrap_geom(area)), height) for area, height in old_geoms.heightareas) if not area.is_empty ) - new_geoms.affected_area = unary_union(( - *(altitudearea.geometry for altitudearea in new_geoms.altitudeareas), - crop_to.intersection(new_geoms.walls.buffer(1)), - *((new_geoms.holes.buffer(1),) if new_geoms.holes else ()), - )) - for access_restriction, area in old_geoms.access_restriction_affected.items(): new_area = crop_to.intersection(area) if not new_area.is_empty: access_restriction_affected.setdefault(access_restriction, []).append(new_area) - new_geoms.restricted_spaces_indoors = {} + new_restricted_spaces_indoors = {} for access_restriction, area in old_geoms.restricted_spaces_indoors.items(): new_area = crop_to.intersection(area) if not new_area.is_empty: - new_geoms.restricted_spaces_indoors[access_restriction] = new_area + new_restricted_spaces_indoors[access_restriction] = new_area - new_geoms.restricted_spaces_outdoors = {} + new_restricted_spaces_outdoors = {} for access_restriction, area in old_geoms.restricted_spaces_outdoors.items(): new_area = crop_to.intersection(area) if not new_area.is_empty: - new_geoms.restricted_spaces_outdoors[access_restriction] = new_area + new_restricted_spaces_outdoors[access_restriction] = new_area - new_geoms.pk = old_geoms.pk - new_geoms.on_top_of_id = old_geoms.on_top_of_id - new_geoms.short_label = old_geoms.short_label - new_geoms.base_altitude = old_geoms.base_altitude - new_geoms.default_height = old_geoms.default_height - new_geoms.door_height = old_geoms.door_height - new_geoms.min_altitude = (min(area.min_altitude for area in new_geoms.altitudeareas) - if new_geoms.altitudeareas else new_geoms.base_altitude) - new_geoms.max_altitude = (max(area.max_altitude for area in new_geoms.altitudeareas) - if new_geoms.altitudeareas else new_geoms.base_altitude) - new_geoms.max_height = (min(height for area, height in new_geoms.heightareas) - if new_geoms.heightareas else new_geoms.default_height) - new_geoms.lower_bound = old_geoms.lower_bound - new_geoms.upper_bound = old_geoms.upper_bound + new_geoms = LevelGeometries( + pk=old_geoms.pk, + on_top_of_id=old_geoms.on_top_of_id, + short_label=old_geoms.short_label, + base_altitude=old_geoms.base_altitude, + default_height=old_geoms.default_height, + door_height=old_geoms.door_height, + min_altitude=(min(area.min_altitude for area in new_altitudeareas) + if new_altitudeareas else old_geoms.base_altitude), + max_altitude=(max(area.max_altitude for area in new_altitudeareas) + if new_altitudeareas else old_geoms.base_altitude), + max_height=(min(height for area, height in new_heightareas) + if new_heightareas else old_geoms.default_height), + lower_bound=old_geoms.lower_bound, + upper_bound=old_geoms.upper_bound, + heightareas=new_heightareas, + altitudeareas=new_altitudeareas, + + buildings=new_buildings_geoms, + holes=new_holes_geoms, + doors=new_doors_geoms, + walls=new_walls_geoms, + all_walls=new_all_walls_geoms, + short_walls=new_short_walls_geoms, + + restricted_spaces_indoors=new_restricted_spaces_indoors, + restricted_spaces_outdoors=new_restricted_spaces_outdoors, + + ramps=tuple( + ramp for ramp in (crop_to.intersection(unwrap_geom(ramp)) for ramp in old_geoms.ramps) + if not ramp.is_empty + ), + + affected_area=unary_union(( + *(altitudearea.geometry for altitudearea in new_altitudeareas), + crop_to.intersection(new_walls_geoms.buffer(1)), + *((new_holes_geoms.buffer(1),) if new_holes_geoms else ()), + )), + + access_restriction_affected=None, + doors_extended=None, + faces=None, + vertices=None, + walls_base=None, + walls_bottom=None, + walls_extended=None, + ) new_geoms.build_mesh(interpolators.get(render_level.pk) if level.pk == render_level.pk else None)