make LevelRenderData into a dataclass

This commit is contained in:
Laura Klünder 2024-08-19 15:19:12 +02:00
parent 034f8fe463
commit 9f264d1f59
3 changed files with 215 additions and 153 deletions

View file

@ -26,11 +26,11 @@ class AltitudeAreaGeometries:
self.obstacles = obstacles self.obstacles = obstacles
@property @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) return self.altitude if self.altitude is not None else min(p.altitude for p in self.points)
@property @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) return self.altitude if self.altitude is not None else max(p.altitude for p in self.points)
def get_altitudes(self, points): def get_altitudes(self, points):

View file

@ -6,12 +6,11 @@ from functools import reduce
from itertools import chain from itertools import chain
import numpy as np import numpy as np
from django.db.backends.ddl_references import Columns
from shapely import prepared from shapely import prepared
from shapely.geometry import GeometryCollection, Polygon, MultiPolygon from shapely.geometry import GeometryCollection, Polygon, MultiPolygon
from shapely.ops import unary_union 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.altitudearea import AltitudeAreaGeometries
from c3nav.mapdata.render.geometry.hybrid import HybridGeometry from c3nav.mapdata.render.geometry.hybrid import HybridGeometry
from c3nav.mapdata.render.geometry.mesh import Mesh from c3nav.mapdata.render.geometry.mesh import Mesh
@ -28,45 +27,46 @@ empty_geometry_collection = GeometryCollection()
ZeroOrMorePolygons: typing.TypeAlias = GeometryCollection | Polygon | MultiPolygon ZeroOrMorePolygons: typing.TypeAlias = GeometryCollection | Polygon | MultiPolygon
@dataclass
class LevelGeometries: class LevelGeometries:
""" """
Store geometries for a Level. Store geometries for a Level.
""" """
def __init__(self): # todo: split into the two versions of this
self.buildings = None buildings: ZeroOrMorePolygons
self.altitudeareas = [] altitudeareas: list[AltitudeAreaGeometries]
self.heightareas = [] heightareas: typing.Sequence[tuple[ZeroOrMorePolygons, float]]
self.walls = None walls: ZeroOrMorePolygons
self.walls_extended = None walls_extended: None | HybridGeometry
self.all_walls = None all_walls: ZeroOrMorePolygons
self.short_walls = [] short_walls: list[tuple[AltitudeArea, ZeroOrMorePolygons]] | typing.Sequence[ZeroOrMorePolygons]
self.doors = None doors: ZeroOrMorePolygons | None
self.doors_extended = None doors_extended: HybridGeometry | None
self.holes = None holes: None
self.access_restriction_affected = None access_restriction_affected: dict[int, ZeroOrMorePolygons] | None
self.restricted_spaces_indoors = None restricted_spaces_indoors: dict[int, ZeroOrMorePolygons]
self.restricted_spaces_outdoors = None restricted_spaces_outdoors: dict[int, ZeroOrMorePolygons]
self.affected_area = None affected_area: ZeroOrMorePolygons | None
self.ramps = [] ramps: typing.Sequence[ZeroOrMorePolygons]
self.vertices = None vertices: None | np.ndarray
self.faces = None faces: None | np.ndarray
self.walls_base = None walls_base: None | HybridGeometry
self.walls_bottom = None walls_bottom: None | HybridGeometry
self.pk = None pk: int
self.on_top_of_id = None on_top_of_id: int | None
self.short_label = None short_label: str
self.base_altitude = None base_altitude: int
self.default_height = None default_height: int
self.door_height = None door_height: int
self.min_altitude = None min_altitude: int
self.max_altitude = None max_altitude: int
self.max_height = None max_height: int
self.lower_bound = None lower_bound: int
self.upper_bound = None upper_bound: None
def __repr__(self): def __repr__(self):
return '<LevelGeometries for Level %s (#%d)>' % (self.short_label, self.pk) return '<LevelGeometries for Level %s (#%d)>' % (self.short_label, self.pk)
@ -80,9 +80,9 @@ class LevelGeometries:
instance: Space instance: Space
@classmethod @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] = [] spaces: list[cls.SpaceGeometries] = []
for space in level.spaces.all(): for space in level.spaces.all(): # noqa
geometry = space.geometry geometry = space.geometry
subtract = [] subtract = []
if space.outside: if space.outside:
@ -124,7 +124,7 @@ class LevelGeometries:
ramps: list[ZeroOrMorePolygons] ramps: list[ZeroOrMorePolygons]
@classmethod @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: ZeroOrMorePolygons, color_manager: 'ThemeColorManager') -> Analysis:
buildings_geom_prep = prepared.prep(buildings_geom) buildings_geom_prep = prepared.prep(buildings_geom)
@ -145,7 +145,7 @@ class LevelGeometries:
for space in spaces: for space in spaces:
buffered = space.geometry.buffer(0.01).union(unary_union(tuple( buffered = space.geometry.buffer(0.01).union(unary_union(tuple(
unwrap_geom(door.geometry) 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)) )).difference(walkable_spaces_geom))
intersects = buildings_geom_prep.intersects(buffered) intersects = buildings_geom_prep.intersects(buffered)
@ -236,36 +236,12 @@ class LevelGeometries:
ramps=ramps, ramps=ramps,
) )
@classmethod @classmethod
def build_for_level(cls, level, color_manager: 'ThemeColorManager', altitudeareas_above): def build_altitudeareas(cls, level: Level, analysis: Analysis) -> list[AltitudeAreaGeometries]:
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
# add altitudegroup geometries and split ground colors into them # 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_prep = prepared.prep(unwrap_geom(altitudearea.geometry))
altitudearea_colors = {color: {access_restriction: area.intersection(unwrap_geom(altitudearea.geometry)) altitudearea_colors = {color: {access_restriction: area.intersection(unwrap_geom(altitudearea.geometry))
for access_restriction, area in areas.items() for access_restriction, area in areas.items()
@ -286,59 +262,121 @@ class LevelGeometries:
if new_height_obstacles: if new_height_obstacles:
altitudearea_obstacles[height] = new_height_obstacles altitudearea_obstacles[height] = new_height_obstacles
geoms.altitudeareas.append(AltitudeAreaGeometries(altitudearea, altitudearea_geoms.append(AltitudeAreaGeometries(
altitudearea_colors, altitudearea=altitudearea,
altitudearea_obstacles)) colors=altitudearea_colors,
obstacles=altitudearea_obstacles
# merge height areas ))
geoms.heightareas = tuple((unary_union(geoms), height) return altitudearea_geoms
for height, geoms in sorted(analysis.heightareas.items(), key=operator.itemgetter(0)))
@classmethod
# merge access restrictions def build_short_walls(cls, altitudeareas_above,
geoms.access_restriction_affected = { walls_geom: ZeroOrMorePolygons) -> list[tuple[AltitudeArea, ZeroOrMorePolygons]]:
access_restriction: unary_union([unwrap_geom(geom) for geom in areas]) remaining = walls_geom
for access_restriction, areas in analysis.access_restriction_affected.items() short_walls = []
}
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
for altitudearea in altitudeareas_above: for altitudearea in altitudeareas_above:
intersection = altitudearea.geometry.intersection(remaining).buffer(0) intersection = altitudearea.geometry.intersection(remaining).buffer(0)
if intersection.is_empty: if intersection.is_empty:
continue continue
remaining = remaining.difference(unwrap_geom(altitudearea.geometry)) remaining = remaining.difference(unwrap_geom(altitudearea.geometry))
geoms.short_walls.append((altitudearea, intersection)) short_walls.append((altitudearea, intersection))
geoms.all_walls = geoms.walls return short_walls
geoms.walls = geoms.walls.difference(
unary_union(tuple(unwrap_geom(altitudearea.geometry) for altitudearea in altitudeareas_above)) @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 altitudearea_geoms = cls.build_altitudeareas(level=level, analysis=analysis)
geoms.pk = level.pk heightareas_geom = tuple((unary_union(geoms), height) for height, geoms in
geoms.on_top_of_id = level.on_top_of_id sorted(analysis.heightareas.items(), key=operator.itemgetter(0)))
geoms.short_label = level.short_label
geoms.base_altitude = int(level.base_altitude * 1000) base_altitude = int(level.base_altitude * 1000)
geoms.default_height = int(level.default_height * 1000) default_height = int(level.default_height * 1000)
geoms.door_height = int(level.door_height * 1000) 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) min_altitude = (min(area.min_altitude for area in altitudearea_geoms) if altitudearea_geoms else base_altitude)
geoms.max_altitude = (max(area.max_altitude for area in geoms.altitudeareas)
if geoms.altitudeareas else geoms.base_altitude) # hybrid geometries
geoms.max_height = (min(height for area, height in geoms.heightareas)
if geoms.heightareas else geoms.default_height) geoms = cls(
geoms.lower_bound = geoms.min_altitude-700 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 return geoms
@ -388,7 +426,7 @@ class LevelGeometries:
vertex_values = np.empty(self.vertices.shape[:1], dtype=np.int32) vertex_values = np.empty(self.vertices.shape[:1], dtype=np.int32)
if not vertex_values.size: if not vertex_values.size:
return vertex_values 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: for item in items:
faces = area_func(item).faces faces = area_func(item).faces

View file

@ -206,18 +206,20 @@ class LevelRenderData:
elif render_level.pk != level.pk: elif render_level.pk != level.pk:
map_history.composite(MapHistory.open_level(level.pk, 'base'), None) map_history.composite(MapHistory.open_level(level.pk, 'base'), None)
new_geoms = LevelGeometries() new_buildings_geoms = crop_to.intersection(old_geoms.buildings)
new_geoms.buildings = crop_to.intersection(old_geoms.buildings)
if old_geoms.on_top_of_id is None: if old_geoms.on_top_of_id is None:
new_geoms.holes = crop_to.intersection(old_geoms.holes) new_holes_geoms = crop_to.intersection(old_geoms.holes)
new_geoms.doors = crop_to.intersection(old_geoms.doors) else:
new_geoms.walls = crop_to.intersection(old_geoms.walls) new_holes_geoms = None
new_geoms.all_walls = crop_to.intersection(old_geoms.all_walls) new_doors_geoms = crop_to.intersection(old_geoms.doors)
new_geoms.short_walls = tuple((altitude, geom) for altitude, geom in tuple( 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)) (altitude, crop_to.intersection(geom))
for altitude, geom in old_geoms.short_walls for altitude, geom in old_geoms.short_walls
) if not geom.is_empty) ) if not geom.is_empty)
new_altitudeareas = []
for altitudearea in old_geoms.altitudeareas: for altitudearea in old_geoms.altitudeareas:
new_geometry = crop_to.intersection(unwrap_geom(altitudearea.geometry)) new_geometry = crop_to.intersection(unwrap_geom(altitudearea.geometry))
if new_geometry.is_empty: if new_geometry.is_empty:
@ -258,59 +260,81 @@ class LevelRenderData:
new_altitudearea_obstacles[height] = new_height_obstacles new_altitudearea_obstacles[height] = new_height_obstacles
new_altitudearea.obstacles = new_altitudearea_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 continue
new_geoms.ramps = tuple( new_heightareas = 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(
(area, height) for area, height in ((crop_to.intersection(unwrap_geom(area)), height) (area, height) for area, height in ((crop_to.intersection(unwrap_geom(area)), height)
for area, height in old_geoms.heightareas) for area, height in old_geoms.heightareas)
if not area.is_empty 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(): for access_restriction, area in old_geoms.access_restriction_affected.items():
new_area = crop_to.intersection(area) new_area = crop_to.intersection(area)
if not new_area.is_empty: if not new_area.is_empty:
access_restriction_affected.setdefault(access_restriction, []).append(new_area) 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(): for access_restriction, area in old_geoms.restricted_spaces_indoors.items():
new_area = crop_to.intersection(area) new_area = crop_to.intersection(area)
if not new_area.is_empty: 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(): for access_restriction, area in old_geoms.restricted_spaces_outdoors.items():
new_area = crop_to.intersection(area) new_area = crop_to.intersection(area)
if not new_area.is_empty: 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 = LevelGeometries(
new_geoms.on_top_of_id = old_geoms.on_top_of_id pk=old_geoms.pk,
new_geoms.short_label = old_geoms.short_label on_top_of_id=old_geoms.on_top_of_id,
new_geoms.base_altitude = old_geoms.base_altitude short_label=old_geoms.short_label,
new_geoms.default_height = old_geoms.default_height base_altitude=old_geoms.base_altitude,
new_geoms.door_height = old_geoms.door_height default_height=old_geoms.default_height,
new_geoms.min_altitude = (min(area.min_altitude for area in new_geoms.altitudeareas) door_height=old_geoms.door_height,
if new_geoms.altitudeareas else new_geoms.base_altitude) min_altitude=(min(area.min_altitude for area in new_altitudeareas)
new_geoms.max_altitude = (max(area.max_altitude for area in new_geoms.altitudeareas) if new_altitudeareas else old_geoms.base_altitude),
if new_geoms.altitudeareas else new_geoms.base_altitude) max_altitude=(max(area.max_altitude for area in new_altitudeareas)
new_geoms.max_height = (min(height for area, height in new_geoms.heightareas) if new_altitudeareas else old_geoms.base_altitude),
if new_geoms.heightareas else new_geoms.default_height) max_height=(min(height for area, height in new_heightareas)
new_geoms.lower_bound = old_geoms.lower_bound if new_heightareas else old_geoms.default_height),
new_geoms.upper_bound = old_geoms.upper_bound 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) new_geoms.build_mesh(interpolators.get(render_level.pk) if level.pk == render_level.pk else None)