split up c3nav.mapdata.render.data
This commit is contained in:
parent
708620db0a
commit
6bad1f9cc3
6 changed files with 495 additions and 456 deletions
2
src/c3nav/mapdata/render/data/__init__.py
Normal file
2
src/c3nav/mapdata/render/data/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from c3nav.mapdata.render.data.hybridgeom import hybrid_union, HybridGeometry # noqa
|
||||
from c3nav.mapdata.render.data.levelrender import LevelRenderData # noqa
|
97
src/c3nav/mapdata/render/data/altitudearea.py
Normal file
97
src/c3nav/mapdata/render/data/altitudearea.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
from collections import deque
|
||||
from itertools import chain
|
||||
|
||||
import numpy as np
|
||||
|
||||
from c3nav.mapdata.models import AltitudeArea
|
||||
from c3nav.mapdata.render.data.hybridgeom import HybridGeometry
|
||||
|
||||
|
||||
class AltitudeAreaGeometries:
|
||||
def __init__(self, altitudearea=None, colors=None, obstacles=None):
|
||||
if altitudearea is not None:
|
||||
self.geometry = altitudearea.geometry
|
||||
self.altitude = int(altitudearea.altitude * 1000)
|
||||
self.altitude2 = None if altitudearea.altitude2 is None else int(altitudearea.altitude2 * 1000)
|
||||
self.point1 = altitudearea.point1
|
||||
self.point2 = altitudearea.point2
|
||||
else:
|
||||
self.geometry = None
|
||||
self.altitude = None
|
||||
self.altitude2 = None
|
||||
self.point1 = None
|
||||
self.point2 = None
|
||||
self.base = None
|
||||
self.bottom = None
|
||||
self.colors = colors
|
||||
self.obstacles = obstacles
|
||||
|
||||
def get_altitudes(self, points):
|
||||
# noinspection PyCallByClass,PyTypeChecker
|
||||
return AltitudeArea.get_altitudes(self, points/1000).astype(np.int32)
|
||||
|
||||
def create_hybrid_geometries(self, face_centers, vertices_offset, faces_offset):
|
||||
self.geometry = HybridGeometry.create(self.geometry, face_centers)
|
||||
|
||||
vertices = deque()
|
||||
faces = deque()
|
||||
|
||||
for color, areas in self.colors.items():
|
||||
for key in tuple(areas.keys()):
|
||||
faces_offset, vertices_offset = self._call_create_full(areas, key, faces, vertices,
|
||||
faces_offset, vertices_offset)
|
||||
|
||||
for key in tuple(self.obstacles.keys()):
|
||||
faces_offset, vertices_offset = self._call_create_full(self.obstacles, key, faces, vertices,
|
||||
faces_offset, vertices_offset)
|
||||
|
||||
if not vertices:
|
||||
return np.empty((0, 2), dtype=np.int32), np.empty((0, 3), dtype=np.uint32)
|
||||
return np.vstack(vertices), np.vstack(faces)
|
||||
|
||||
def _call_create_full(self, mapping, key, faces, vertices, faces_offset, vertices_offset):
|
||||
geom = mapping[key]
|
||||
new_geom, new_vertices, new_faces = HybridGeometry.create_full(geom, vertices_offset, faces_offset)
|
||||
mapping[key] = new_geom
|
||||
vertices_offset += new_vertices.shape[0]
|
||||
faces_offset += new_faces.shape[0]
|
||||
vertices.append(new_vertices)
|
||||
faces.append(new_faces)
|
||||
return faces_offset, vertices_offset
|
||||
|
||||
def remove_faces(self, faces):
|
||||
self.geometry.remove_faces(faces)
|
||||
for areas in self.colors.values():
|
||||
for area in areas.values():
|
||||
area.remove_faces(faces)
|
||||
|
||||
def create_polyhedrons(self, create_polyhedron, altitudes, min_altitude, crops):
|
||||
if self.altitude2 is None:
|
||||
altitudes = self.altitude
|
||||
|
||||
self.base = HybridGeometry(self.geometry.geom, self.geometry.faces)
|
||||
self.bottom = HybridGeometry(self.geometry.geom, self.geometry.faces)
|
||||
self.geometry.build_polyhedron(create_polyhedron,
|
||||
lower=altitudes - int(0.7 * 1000),
|
||||
upper=altitudes,
|
||||
crops=crops)
|
||||
self.base.build_polyhedron(create_polyhedron,
|
||||
lower=min_altitude - int(0.7 * 1000),
|
||||
upper=altitudes - int(0.7 * 1000),
|
||||
crops=crops,
|
||||
top=False, bottom=False)
|
||||
self.bottom.build_polyhedron(create_polyhedron,
|
||||
lower=0, upper=1,
|
||||
crops=crops,
|
||||
top=False)
|
||||
|
||||
for geometry in chain(*(areas.values() for areas in self.colors.values())):
|
||||
geometry.build_polyhedron(create_polyhedron,
|
||||
lower=altitudes,
|
||||
upper=altitudes + int(0.001 * 1000),
|
||||
crops=crops)
|
||||
for height, geometry in self.obstacles.items():
|
||||
geometry.build_polyhedron(create_polyhedron,
|
||||
lower=altitudes,
|
||||
upper=altitudes + height,
|
||||
crops=crops)
|
114
src/c3nav/mapdata/render/data/hybridgeom.py
Normal file
114
src/c3nav/mapdata/render/data/hybridgeom.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
import operator
|
||||
from collections import deque
|
||||
from functools import reduce
|
||||
from itertools import chain
|
||||
|
||||
import numpy as np
|
||||
from shapely.geometry import GeometryCollection, LineString, MultiLineString
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from c3nav.mapdata.utils.geometry import assert_multipolygon
|
||||
from c3nav.mapdata.utils.mesh import triangulate_polygon
|
||||
from c3nav.mapdata.utils.mpl import shapely_to_mpl
|
||||
|
||||
|
||||
def hybrid_union(geoms):
|
||||
if not geoms:
|
||||
return HybridGeometry(GeometryCollection(), ())
|
||||
if len(geoms) == 1:
|
||||
return geoms[0]
|
||||
add_faces = {}
|
||||
for other in geoms:
|
||||
for crop_id, faces in other.add_faces.items():
|
||||
add_faces[crop_id] = add_faces.get(crop_id, ()) + faces
|
||||
return HybridGeometry(geom=unary_union(tuple(geom.geom for geom in geoms)),
|
||||
faces=tuple(chain(*(geom.faces for geom in geoms))),
|
||||
add_faces=add_faces,
|
||||
crop_ids=reduce(operator.or_, (other.crop_ids for other in geoms), set()))
|
||||
|
||||
|
||||
class HybridGeometry:
|
||||
__slots__ = ('geom', 'faces', 'crop_ids', 'add_faces')
|
||||
|
||||
def __init__(self, geom, faces, crop_ids=frozenset(), add_faces=None):
|
||||
self.geom = geom
|
||||
self.faces = faces
|
||||
self.add_faces = add_faces or {}
|
||||
self.crop_ids = crop_ids
|
||||
|
||||
@classmethod
|
||||
def create(cls, geom, face_centers):
|
||||
if isinstance(geom, (LineString, MultiLineString)):
|
||||
return HybridGeometry(geom, set())
|
||||
faces = tuple(
|
||||
set(np.argwhere(shapely_to_mpl(subgeom).contains_points(face_centers)).flatten())
|
||||
for subgeom in assert_multipolygon(geom)
|
||||
)
|
||||
return HybridGeometry(geom, tuple(f for f in faces if f))
|
||||
|
||||
@classmethod
|
||||
def create_full(cls, geom, vertices_offset, faces_offset):
|
||||
if isinstance(geom, (LineString, MultiLineString)):
|
||||
return HybridGeometry(geom, set()), np.empty((0, 2), dtype=np.int32), np.empty((0, 3), dtype=np.uint32)
|
||||
|
||||
vertices = deque()
|
||||
faces = deque()
|
||||
faces_i = deque()
|
||||
for subgeom in assert_multipolygon(geom):
|
||||
new_vertices, new_faces = triangulate_polygon(subgeom)
|
||||
new_faces += vertices_offset
|
||||
vertices.append(new_vertices)
|
||||
faces.append(new_faces)
|
||||
faces_i.append(set(range(faces_offset, faces_offset+new_faces.shape[0])))
|
||||
vertices_offset += new_vertices.shape[0]
|
||||
faces_offset += new_faces.shape[0]
|
||||
|
||||
vertices = np.vstack(vertices)
|
||||
faces = np.vstack(faces)
|
||||
|
||||
return HybridGeometry(geom, tuple(faces_i)), vertices, faces
|
||||
|
||||
def union(self, other):
|
||||
add_faces = self.add_faces
|
||||
for crop_id, faces in other.add_faces.items():
|
||||
add_faces[crop_id] = add_faces.get(crop_id, ())+faces
|
||||
return HybridGeometry(geom=self.geom.union(other.geom), faces=self.faces+other.faces, add_faces=add_faces,
|
||||
crop_ids=self.crop_ids | other.crop_ids)
|
||||
|
||||
def difference(self, other):
|
||||
return HybridGeometry(geom=self.geom.difference(other.geom), faces=self.faces,
|
||||
add_faces={crop_id: faces for crop_id, faces in self.add_faces.items()
|
||||
if crop_id not in other.crop_ids},
|
||||
crop_ids=self.crop_ids - other.crop_ids)
|
||||
|
||||
def fit(self, scale, offset):
|
||||
offset = np.array((0, 0, offset))
|
||||
scale = np.array((1, 1, scale))
|
||||
return HybridGeometry(geom=self.geom, crop_ids=self.crop_ids,
|
||||
faces=tuple((faces*scale+offset) for faces in self.faces),
|
||||
add_faces={crop_id: tuple((faces*scale+offset) for faces in self.faces)
|
||||
for crop_id, faces in self.add_faces})
|
||||
|
||||
def filter(self, **kwargs):
|
||||
return HybridGeometry(geom=self.geom, crop_ids=self.crop_ids,
|
||||
faces=tuple(mesh.filter(**kwargs) for mesh in self.faces),
|
||||
add_faces={crop_id: tuple(mesh.filter(**kwargs) for mesh in faces)
|
||||
for crop_id, faces in self.add_faces.items()})
|
||||
|
||||
def remove_faces(self, faces):
|
||||
self.faces = tuple((subfaces-faces) for subfaces in self.faces)
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return not self.faces and not any(self.add_faces.values())
|
||||
|
||||
def build_polyhedron(self, create_polyhedron, crops=None, **kwargs):
|
||||
remaining_faces = self.faces
|
||||
for crop, prep in crops or ():
|
||||
if prep.intersects(self.geom):
|
||||
crop_faces = set(chain(*crop.faces))
|
||||
crop_id = tuple(crop.crop_ids)[0]
|
||||
self.add_faces[crop_id] = create_polyhedron(tuple((faces & crop_faces)
|
||||
for faces in self.faces), **kwargs)
|
||||
remaining_faces = tuple((faces - crop_faces) for faces in self.faces)
|
||||
self.faces = create_polyhedron(remaining_faces, **kwargs)
|
|
@ -1,471 +1,23 @@
|
|||
import operator
|
||||
import os
|
||||
import pickle
|
||||
import threading
|
||||
from collections import Counter, deque, namedtuple
|
||||
from collections import Counter, deque
|
||||
from functools import reduce
|
||||
from itertools import chain
|
||||
|
||||
import numpy as np
|
||||
from django.conf import settings
|
||||
from scipy.interpolate import NearestNDInterpolator
|
||||
from shapely import prepared
|
||||
from shapely.geometry import GeometryCollection, LineString, MultiLineString
|
||||
from shapely.geometry import GeometryCollection
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from c3nav.mapdata.models import AltitudeArea, Level, MapUpdate
|
||||
from c3nav.mapdata.utils.cache import MapHistory
|
||||
from c3nav.mapdata.utils.geometry import assert_multipolygon, get_rings
|
||||
from c3nav.mapdata.utils.mesh import triangulate_polygon, triangulate_rings
|
||||
from c3nav.mapdata.utils.mpl import shapely_to_mpl
|
||||
|
||||
|
||||
def hybrid_union(geoms):
|
||||
if not geoms:
|
||||
return HybridGeometry(GeometryCollection(), ())
|
||||
if len(geoms) == 1:
|
||||
return geoms[0]
|
||||
add_faces = {}
|
||||
for other in geoms:
|
||||
for crop_id, faces in other.add_faces.items():
|
||||
add_faces[crop_id] = add_faces.get(crop_id, ()) + faces
|
||||
return HybridGeometry(geom=unary_union(tuple(geom.geom for geom in geoms)),
|
||||
faces=tuple(chain(*(geom.faces for geom in geoms))),
|
||||
add_faces=add_faces,
|
||||
crop_ids=reduce(operator.or_, (other.crop_ids for other in geoms), set()))
|
||||
|
||||
|
||||
class HybridGeometry:
|
||||
__slots__ = ('geom', 'faces', 'crop_ids', 'add_faces')
|
||||
|
||||
def __init__(self, geom, faces, crop_ids=frozenset(), add_faces=None):
|
||||
self.geom = geom
|
||||
self.faces = faces
|
||||
self.add_faces = add_faces or {}
|
||||
self.crop_ids = crop_ids
|
||||
|
||||
@classmethod
|
||||
def create(cls, geom, face_centers):
|
||||
if isinstance(geom, (LineString, MultiLineString)):
|
||||
return HybridGeometry(geom, set())
|
||||
faces = tuple(
|
||||
set(np.argwhere(shapely_to_mpl(subgeom).contains_points(face_centers)).flatten())
|
||||
for subgeom in assert_multipolygon(geom)
|
||||
)
|
||||
return HybridGeometry(geom, tuple(f for f in faces if f))
|
||||
|
||||
@classmethod
|
||||
def create_full(cls, geom, vertices_offset, faces_offset):
|
||||
if isinstance(geom, (LineString, MultiLineString)):
|
||||
return HybridGeometry(geom, set()), np.empty((0, 2), dtype=np.int32), np.empty((0, 3), dtype=np.uint32)
|
||||
|
||||
vertices = deque()
|
||||
faces = deque()
|
||||
faces_i = deque()
|
||||
for subgeom in assert_multipolygon(geom):
|
||||
new_vertices, new_faces = triangulate_polygon(subgeom)
|
||||
new_faces += vertices_offset
|
||||
vertices.append(new_vertices)
|
||||
faces.append(new_faces)
|
||||
faces_i.append(set(range(faces_offset, faces_offset+new_faces.shape[0])))
|
||||
vertices_offset += new_vertices.shape[0]
|
||||
faces_offset += new_faces.shape[0]
|
||||
|
||||
vertices = np.vstack(vertices)
|
||||
faces = np.vstack(faces)
|
||||
|
||||
return HybridGeometry(geom, tuple(faces_i)), vertices, faces
|
||||
|
||||
def union(self, other):
|
||||
add_faces = self.add_faces
|
||||
for crop_id, faces in other.add_faces.items():
|
||||
add_faces[crop_id] = add_faces.get(crop_id, ())+faces
|
||||
return HybridGeometry(geom=self.geom.union(other.geom), faces=self.faces+other.faces, add_faces=add_faces,
|
||||
crop_ids=self.crop_ids | other.crop_ids)
|
||||
|
||||
def difference(self, other):
|
||||
return HybridGeometry(geom=self.geom.difference(other.geom), faces=self.faces,
|
||||
add_faces={crop_id: faces for crop_id, faces in self.add_faces.items()
|
||||
if crop_id not in other.crop_ids},
|
||||
crop_ids=self.crop_ids - other.crop_ids)
|
||||
|
||||
def fit(self, scale, offset):
|
||||
offset = np.array((0, 0, offset))
|
||||
scale = np.array((1, 1, scale))
|
||||
return HybridGeometry(geom=self.geom, crop_ids=self.crop_ids,
|
||||
faces=tuple((faces*scale+offset) for faces in self.faces),
|
||||
add_faces={crop_id: tuple((faces*scale+offset) for faces in self.faces)
|
||||
for crop_id, faces in self.add_faces})
|
||||
|
||||
def filter(self, **kwargs):
|
||||
return HybridGeometry(geom=self.geom, crop_ids=self.crop_ids,
|
||||
faces=tuple(mesh.filter(**kwargs) for mesh in self.faces),
|
||||
add_faces={crop_id: tuple(mesh.filter(**kwargs) for mesh in faces)
|
||||
for crop_id, faces in self.add_faces.items()})
|
||||
|
||||
def remove_faces(self, faces):
|
||||
self.faces = tuple((subfaces-faces) for subfaces in self.faces)
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return not self.faces and not any(self.add_faces.values())
|
||||
|
||||
def build_polyhedron(self, create_polyhedron, crops=None, **kwargs):
|
||||
remaining_faces = self.faces
|
||||
for crop, prep in crops or ():
|
||||
if prep.intersects(self.geom):
|
||||
crop_faces = set(chain(*crop.faces))
|
||||
crop_id = tuple(crop.crop_ids)[0]
|
||||
self.add_faces[crop_id] = create_polyhedron(tuple((faces & crop_faces)
|
||||
for faces in self.faces), **kwargs)
|
||||
remaining_faces = tuple((faces - crop_faces) for faces in self.faces)
|
||||
self.faces = create_polyhedron(remaining_faces, **kwargs)
|
||||
|
||||
|
||||
class AltitudeAreaGeometries:
|
||||
def __init__(self, altitudearea=None, colors=None, obstacles=None):
|
||||
if altitudearea is not None:
|
||||
self.geometry = altitudearea.geometry
|
||||
self.altitude = int(altitudearea.altitude * 1000)
|
||||
self.altitude2 = None if altitudearea.altitude2 is None else int(altitudearea.altitude2 * 1000)
|
||||
self.point1 = altitudearea.point1
|
||||
self.point2 = altitudearea.point2
|
||||
else:
|
||||
self.geometry = None
|
||||
self.altitude = None
|
||||
self.altitude2 = None
|
||||
self.point1 = None
|
||||
self.point2 = None
|
||||
self.base = None
|
||||
self.bottom = None
|
||||
self.colors = colors
|
||||
self.obstacles = obstacles
|
||||
|
||||
def get_altitudes(self, points):
|
||||
# noinspection PyCallByClass,PyTypeChecker
|
||||
return AltitudeArea.get_altitudes(self, points/1000).astype(np.int32)
|
||||
|
||||
def create_hybrid_geometries(self, face_centers, vertices_offset, faces_offset):
|
||||
self.geometry = HybridGeometry.create(self.geometry, face_centers)
|
||||
|
||||
vertices = deque()
|
||||
faces = deque()
|
||||
|
||||
for color, areas in self.colors.items():
|
||||
for key in tuple(areas.keys()):
|
||||
faces_offset, vertices_offset = self._call_create_full(areas, key, faces, vertices,
|
||||
faces_offset, vertices_offset)
|
||||
|
||||
for key in tuple(self.obstacles.keys()):
|
||||
faces_offset, vertices_offset = self._call_create_full(self.obstacles, key, faces, vertices,
|
||||
faces_offset, vertices_offset)
|
||||
|
||||
if not vertices:
|
||||
return np.empty((0, 2), dtype=np.int32), np.empty((0, 3), dtype=np.uint32)
|
||||
return np.vstack(vertices), np.vstack(faces)
|
||||
|
||||
def _call_create_full(self, mapping, key, faces, vertices, faces_offset, vertices_offset):
|
||||
geom = mapping[key]
|
||||
new_geom, new_vertices, new_faces = HybridGeometry.create_full(geom, vertices_offset, faces_offset)
|
||||
mapping[key] = new_geom
|
||||
vertices_offset += new_vertices.shape[0]
|
||||
faces_offset += new_faces.shape[0]
|
||||
vertices.append(new_vertices)
|
||||
faces.append(new_faces)
|
||||
return faces_offset, vertices_offset
|
||||
|
||||
def remove_faces(self, faces):
|
||||
self.geometry.remove_faces(faces)
|
||||
for areas in self.colors.values():
|
||||
for area in areas.values():
|
||||
area.remove_faces(faces)
|
||||
|
||||
def create_polyhedrons(self, create_polyhedron, altitudes, min_altitude, crops):
|
||||
if self.altitude2 is None:
|
||||
altitudes = self.altitude
|
||||
|
||||
self.base = HybridGeometry(self.geometry.geom, self.geometry.faces)
|
||||
self.bottom = HybridGeometry(self.geometry.geom, self.geometry.faces)
|
||||
self.geometry.build_polyhedron(create_polyhedron,
|
||||
lower=altitudes - int(0.7 * 1000),
|
||||
upper=altitudes,
|
||||
crops=crops)
|
||||
self.base.build_polyhedron(create_polyhedron,
|
||||
lower=min_altitude - int(0.7 * 1000),
|
||||
upper=altitudes - int(0.7 * 1000),
|
||||
crops=crops,
|
||||
top=False, bottom=False)
|
||||
self.bottom.build_polyhedron(create_polyhedron,
|
||||
lower=0, upper=1,
|
||||
crops=crops,
|
||||
top=False)
|
||||
|
||||
for geometry in chain(*(areas.values() for areas in self.colors.values())):
|
||||
geometry.build_polyhedron(create_polyhedron,
|
||||
lower=altitudes,
|
||||
upper=altitudes + int(0.001 * 1000),
|
||||
crops=crops)
|
||||
for height, geometry in self.obstacles.items():
|
||||
geometry.build_polyhedron(create_polyhedron,
|
||||
lower=altitudes,
|
||||
upper=altitudes + height,
|
||||
crops=crops)
|
||||
|
||||
from c3nav.mapdata.render.data.altitudearea import AltitudeAreaGeometries
|
||||
from c3nav.mapdata.render.data.hybridgeom import HybridGeometry
|
||||
from c3nav.mapdata.render.data.mesh import Mesh
|
||||
from c3nav.mapdata.utils.geometry import get_rings
|
||||
from c3nav.mapdata.utils.mesh import triangulate_rings
|
||||
|
||||
empty_geometry_collection = GeometryCollection()
|
||||
|
||||
|
||||
class Cropper:
|
||||
def __init__(self, geometry=None):
|
||||
self.geometry = geometry
|
||||
self.geometry_prep = None if geometry is None else prepared.prep(geometry)
|
||||
|
||||
def intersection(self, other):
|
||||
if self.geometry is None:
|
||||
return other
|
||||
if self.geometry_prep.intersects(other):
|
||||
return self.geometry.intersection(other)
|
||||
return empty_geometry_collection
|
||||
|
||||
|
||||
class LevelRenderData:
|
||||
def __init__(self):
|
||||
self.levels = []
|
||||
self.access_restriction_affected = None
|
||||
|
||||
@staticmethod
|
||||
def rebuild():
|
||||
levels = tuple(Level.objects.prefetch_related('altitudeareas', 'buildings', 'doors', 'spaces',
|
||||
'spaces__holes', 'spaces__areas', 'spaces__columns',
|
||||
'spaces__obstacles', 'spaces__lineobstacles',
|
||||
'spaces__groups', 'spaces__ramps'))
|
||||
|
||||
single_level_geoms = {}
|
||||
interpolators = {}
|
||||
last_interpolator = None
|
||||
altitudeareas_above = []
|
||||
for level in reversed(levels):
|
||||
single_level_geoms[level.pk] = LevelGeometries.build_for_level(level, altitudeareas_above)
|
||||
|
||||
if level.on_top_of_id is not None:
|
||||
altitudeareas_above.extend(single_level_geoms[level.pk].altitudeareas)
|
||||
altitudeareas_above.sort(key=operator.attrgetter('altitude'))
|
||||
continue
|
||||
|
||||
if last_interpolator is not None:
|
||||
interpolators[level.pk] = last_interpolator
|
||||
|
||||
coords = deque()
|
||||
values = deque()
|
||||
for area in single_level_geoms[level.pk].altitudeareas:
|
||||
new_coords = np.vstack(tuple(np.array(ring.coords) for ring in get_rings(area.geometry)))
|
||||
coords.append(new_coords)
|
||||
values.append(np.full((new_coords.shape[0], 1), fill_value=area.altitude))
|
||||
|
||||
last_interpolator = NearestNDInterpolator(np.vstack(coords), np.vstack(values))
|
||||
|
||||
for i, level in enumerate(levels):
|
||||
if level.on_top_of_id is not None:
|
||||
continue
|
||||
|
||||
map_history = MapHistory.open_level(level.pk, 'base')
|
||||
|
||||
sublevels = tuple(sublevel for sublevel in levels
|
||||
if sublevel.on_top_of_id == level.pk or sublevel.base_altitude <= level.base_altitude)
|
||||
|
||||
level_crop_to = {}
|
||||
|
||||
# choose a crop area for each level. non-intermediate levels (not on_top_of) below the one that we are
|
||||
# currently rendering will be cropped to only render content that is visible through holes indoors in the
|
||||
# levels above them.
|
||||
crop_to = None
|
||||
primary_level_count = 0
|
||||
for sublevel in reversed(sublevels):
|
||||
geoms = single_level_geoms[sublevel.pk]
|
||||
|
||||
if geoms.holes is not None:
|
||||
primary_level_count += 1
|
||||
|
||||
# set crop area if we area on the second primary layer from top or below
|
||||
level_crop_to[sublevel.pk] = Cropper(crop_to if primary_level_count > 1 else None)
|
||||
|
||||
if geoms.holes is not None:
|
||||
if crop_to is None:
|
||||
crop_to = geoms.holes
|
||||
else:
|
||||
crop_to = crop_to.intersection(geoms.holes)
|
||||
|
||||
if crop_to.is_empty:
|
||||
break
|
||||
|
||||
render_data = LevelRenderData()
|
||||
render_data.access_restriction_affected = {}
|
||||
|
||||
for sublevel in sublevels:
|
||||
try:
|
||||
crop_to = level_crop_to[sublevel.pk]
|
||||
except KeyError:
|
||||
break
|
||||
|
||||
old_geoms = single_level_geoms[sublevel.pk]
|
||||
|
||||
if crop_to.geometry is not None:
|
||||
map_history.composite(MapHistory.open_level(sublevel.pk, 'base'), crop_to.geometry)
|
||||
elif level.pk != sublevel.pk:
|
||||
map_history.composite(MapHistory.open_level(sublevel.pk, 'base'), None)
|
||||
|
||||
new_geoms = LevelGeometries()
|
||||
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(
|
||||
(altitude, crop_to.intersection(geom))
|
||||
for altitude, geom in old_geoms.short_walls
|
||||
) if not geom.is_empty)
|
||||
|
||||
for altitudearea in old_geoms.altitudeareas:
|
||||
new_geometry = crop_to.intersection(altitudearea.geometry)
|
||||
if new_geometry.is_empty:
|
||||
continue
|
||||
new_geometry_prep = prepared.prep(new_geometry)
|
||||
|
||||
new_altitudearea = AltitudeAreaGeometries()
|
||||
new_altitudearea.geometry = new_geometry
|
||||
new_altitudearea.altitude = altitudearea.altitude
|
||||
new_altitudearea.altitude2 = altitudearea.altitude2
|
||||
new_altitudearea.point1 = altitudearea.point1
|
||||
new_altitudearea.point2 = altitudearea.point2
|
||||
|
||||
new_colors = {}
|
||||
for color, areas in altitudearea.colors.items():
|
||||
new_areas = {}
|
||||
for access_restriction, area in areas.items():
|
||||
if not new_geometry_prep.intersects(area):
|
||||
continue
|
||||
new_area = new_geometry.intersection(area)
|
||||
if not new_area.is_empty:
|
||||
new_areas[access_restriction] = new_area
|
||||
if new_areas:
|
||||
new_colors[color] = new_areas
|
||||
new_altitudearea.colors = new_colors
|
||||
|
||||
new_altitudearea.obstacles = {key: new_geometry.intersection(areas)
|
||||
for key, areas in altitudearea.obstacles.items()
|
||||
if new_geometry_prep.intersects(areas)}
|
||||
|
||||
new_geoms.altitudeareas.append(new_altitudearea)
|
||||
|
||||
if new_geoms.walls.is_empty and not new_geoms.altitudeareas:
|
||||
continue
|
||||
|
||||
new_geoms.ramps = tuple(
|
||||
ramp for ramp in (crop_to.intersection(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(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))
|
||||
))
|
||||
|
||||
for access_restriction, area in old_geoms.restricted_spaces_indoors.items():
|
||||
new_area = crop_to.intersection(area)
|
||||
if not new_area.is_empty:
|
||||
render_data.access_restriction_affected.setdefault(access_restriction, []).append(new_area)
|
||||
|
||||
new_geoms.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_geoms.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_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.altitude for area in new_geoms.altitudeareas)
|
||||
if new_geoms.altitudeareas else new_geoms.base_altitude)
|
||||
|
||||
new_geoms.build_mesh(interpolators.get(level.pk) if sublevel.pk == level.pk else None)
|
||||
|
||||
render_data.levels.append(new_geoms)
|
||||
|
||||
render_data.access_restriction_affected = {
|
||||
access_restriction: unary_union(areas)
|
||||
for access_restriction, areas in render_data.access_restriction_affected.items()
|
||||
}
|
||||
|
||||
render_data.save(level.pk)
|
||||
|
||||
map_history.save(MapHistory.level_filename(level.pk, 'composite'))
|
||||
|
||||
cached = {}
|
||||
cache_key = None
|
||||
cache_lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def _level_filename(pk):
|
||||
return os.path.join(settings.CACHE_ROOT, 'level_%d_render_data.pickle' % pk)
|
||||
|
||||
@classmethod
|
||||
def get(cls, level):
|
||||
with cls.cache_lock:
|
||||
cache_key = MapUpdate.current_processed_cache_key()
|
||||
level_pk = str(level.pk if isinstance(level, Level) else level)
|
||||
if cls.cache_key != cache_key:
|
||||
cls.cache_key = cache_key
|
||||
cls.cached = {}
|
||||
else:
|
||||
result = cls.cached.get(level_pk, None)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
pk = level.pk if isinstance(level, Level) else level
|
||||
result = pickle.load(open(cls._level_filename(pk), 'rb'))
|
||||
|
||||
cls.cached[level_pk] = result
|
||||
return result
|
||||
|
||||
def save(self, pk):
|
||||
return pickle.dump(self, open(self._level_filename(pk), 'wb'))
|
||||
|
||||
|
||||
class Mesh(namedtuple('Mesh', ('top', 'sides', 'bottom'))):
|
||||
empty_faces = np.empty((0, 3, 3)).astype(np.int32)
|
||||
|
||||
def tolist(self):
|
||||
return self.top, self.sides, self.bottom
|
||||
|
||||
def __mul__(self, other):
|
||||
return Mesh(top=np.rint(self.top*other).astype(np.int32),
|
||||
sides=np.rint(self.sides*other if other[2] != 0 else self.empty_faces),
|
||||
bottom=np.rint(self.bottom*other).astype(np.int32))
|
||||
|
||||
def __add__(self, other):
|
||||
return Mesh(np.rint(self.top+other).astype(np.int32),
|
||||
np.rint(self.sides+other).astype(np.int32),
|
||||
np.rint(self.bottom+other).astype(np.int32))
|
||||
|
||||
def filter(self, top=True, sides=True, bottom=True):
|
||||
return Mesh(top=self.top if top else self.empty_faces,
|
||||
sides=self.sides if sides else self.empty_faces,
|
||||
bottom=self.bottom if bottom else self.empty_faces)
|
||||
|
||||
|
||||
class LevelGeometries:
|
||||
def __init__(self):
|
||||
self.altitudeareas = []
|
||||
|
@ -789,7 +341,7 @@ class LevelGeometries:
|
|||
else:
|
||||
bottom = Mesh.empty_faces
|
||||
|
||||
return tuple((Mesh(top, sides, bottom), ))
|
||||
return tuple((Mesh(top, sides, bottom),))
|
||||
|
||||
def build_mesh(self, interpolator=None):
|
||||
rings = tuple(chain(*(get_rings(geom) for geom in self.get_geometries())))
|
249
src/c3nav/mapdata/render/data/levelrender.py
Normal file
249
src/c3nav/mapdata/render/data/levelrender.py
Normal file
|
@ -0,0 +1,249 @@
|
|||
import operator
|
||||
import os
|
||||
import pickle
|
||||
import threading
|
||||
from collections import deque
|
||||
|
||||
import numpy as np
|
||||
from django.conf import settings
|
||||
from scipy.interpolate import NearestNDInterpolator
|
||||
from shapely import prepared
|
||||
from shapely.geometry import GeometryCollection
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from c3nav.mapdata.models import Level, MapUpdate
|
||||
from c3nav.mapdata.render.data.altitudearea import AltitudeAreaGeometries
|
||||
from c3nav.mapdata.render.data.levelgeom import LevelGeometries
|
||||
from c3nav.mapdata.utils.cache import MapHistory
|
||||
from c3nav.mapdata.utils.geometry import get_rings
|
||||
|
||||
empty_geometry_collection = GeometryCollection()
|
||||
|
||||
|
||||
class Cropper:
|
||||
def __init__(self, geometry=None):
|
||||
self.geometry = geometry
|
||||
self.geometry_prep = None if geometry is None else prepared.prep(geometry)
|
||||
|
||||
def intersection(self, other):
|
||||
if self.geometry is None:
|
||||
return other
|
||||
if self.geometry_prep.intersects(other):
|
||||
return self.geometry.intersection(other)
|
||||
return empty_geometry_collection
|
||||
|
||||
|
||||
class LevelRenderData:
|
||||
def __init__(self):
|
||||
self.levels = []
|
||||
self.access_restriction_affected = None
|
||||
|
||||
@staticmethod
|
||||
def rebuild():
|
||||
levels = tuple(Level.objects.prefetch_related('altitudeareas', 'buildings', 'doors', 'spaces',
|
||||
'spaces__holes', 'spaces__areas', 'spaces__columns',
|
||||
'spaces__obstacles', 'spaces__lineobstacles',
|
||||
'spaces__groups', 'spaces__ramps'))
|
||||
|
||||
single_level_geoms = {}
|
||||
interpolators = {}
|
||||
last_interpolator = None
|
||||
altitudeareas_above = []
|
||||
for level in reversed(levels):
|
||||
single_level_geoms[level.pk] = LevelGeometries.build_for_level(level, altitudeareas_above)
|
||||
|
||||
if level.on_top_of_id is not None:
|
||||
altitudeareas_above.extend(single_level_geoms[level.pk].altitudeareas)
|
||||
altitudeareas_above.sort(key=operator.attrgetter('altitude'))
|
||||
continue
|
||||
|
||||
if last_interpolator is not None:
|
||||
interpolators[level.pk] = last_interpolator
|
||||
|
||||
coords = deque()
|
||||
values = deque()
|
||||
for area in single_level_geoms[level.pk].altitudeareas:
|
||||
new_coords = np.vstack(tuple(np.array(ring.coords) for ring in get_rings(area.geometry)))
|
||||
coords.append(new_coords)
|
||||
values.append(np.full((new_coords.shape[0], 1), fill_value=area.altitude))
|
||||
|
||||
last_interpolator = NearestNDInterpolator(np.vstack(coords), np.vstack(values))
|
||||
|
||||
for i, level in enumerate(levels):
|
||||
if level.on_top_of_id is not None:
|
||||
continue
|
||||
|
||||
map_history = MapHistory.open_level(level.pk, 'base')
|
||||
|
||||
sublevels = tuple(sublevel for sublevel in levels
|
||||
if sublevel.on_top_of_id == level.pk or sublevel.base_altitude <= level.base_altitude)
|
||||
|
||||
level_crop_to = {}
|
||||
|
||||
# choose a crop area for each level. non-intermediate levels (not on_top_of) below the one that we are
|
||||
# currently rendering will be cropped to only render content that is visible through holes indoors in the
|
||||
# levels above them.
|
||||
crop_to = None
|
||||
primary_level_count = 0
|
||||
for sublevel in reversed(sublevels):
|
||||
geoms = single_level_geoms[sublevel.pk]
|
||||
|
||||
if geoms.holes is not None:
|
||||
primary_level_count += 1
|
||||
|
||||
# set crop area if we area on the second primary layer from top or below
|
||||
level_crop_to[sublevel.pk] = Cropper(crop_to if primary_level_count > 1 else None)
|
||||
|
||||
if geoms.holes is not None:
|
||||
if crop_to is None:
|
||||
crop_to = geoms.holes
|
||||
else:
|
||||
crop_to = crop_to.intersection(geoms.holes)
|
||||
|
||||
if crop_to.is_empty:
|
||||
break
|
||||
|
||||
render_data = LevelRenderData()
|
||||
render_data.access_restriction_affected = {}
|
||||
|
||||
for sublevel in sublevels:
|
||||
try:
|
||||
crop_to = level_crop_to[sublevel.pk]
|
||||
except KeyError:
|
||||
break
|
||||
|
||||
old_geoms = single_level_geoms[sublevel.pk]
|
||||
|
||||
if crop_to.geometry is not None:
|
||||
map_history.composite(MapHistory.open_level(sublevel.pk, 'base'), crop_to.geometry)
|
||||
elif level.pk != sublevel.pk:
|
||||
map_history.composite(MapHistory.open_level(sublevel.pk, 'base'), None)
|
||||
|
||||
new_geoms = LevelGeometries()
|
||||
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(
|
||||
(altitude, crop_to.intersection(geom))
|
||||
for altitude, geom in old_geoms.short_walls
|
||||
) if not geom.is_empty)
|
||||
|
||||
for altitudearea in old_geoms.altitudeareas:
|
||||
new_geometry = crop_to.intersection(altitudearea.geometry)
|
||||
if new_geometry.is_empty:
|
||||
continue
|
||||
new_geometry_prep = prepared.prep(new_geometry)
|
||||
|
||||
new_altitudearea = AltitudeAreaGeometries()
|
||||
new_altitudearea.geometry = new_geometry
|
||||
new_altitudearea.altitude = altitudearea.altitude
|
||||
new_altitudearea.altitude2 = altitudearea.altitude2
|
||||
new_altitudearea.point1 = altitudearea.point1
|
||||
new_altitudearea.point2 = altitudearea.point2
|
||||
|
||||
new_colors = {}
|
||||
for color, areas in altitudearea.colors.items():
|
||||
new_areas = {}
|
||||
for access_restriction, area in areas.items():
|
||||
if not new_geometry_prep.intersects(area):
|
||||
continue
|
||||
new_area = new_geometry.intersection(area)
|
||||
if not new_area.is_empty:
|
||||
new_areas[access_restriction] = new_area
|
||||
if new_areas:
|
||||
new_colors[color] = new_areas
|
||||
new_altitudearea.colors = new_colors
|
||||
|
||||
new_altitudearea.obstacles = {key: new_geometry.intersection(areas)
|
||||
for key, areas in altitudearea.obstacles.items()
|
||||
if new_geometry_prep.intersects(areas)}
|
||||
|
||||
new_geoms.altitudeareas.append(new_altitudearea)
|
||||
|
||||
if new_geoms.walls.is_empty and not new_geoms.altitudeareas:
|
||||
continue
|
||||
|
||||
new_geoms.ramps = tuple(
|
||||
ramp for ramp in (crop_to.intersection(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(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))
|
||||
))
|
||||
|
||||
for access_restriction, area in old_geoms.restricted_spaces_indoors.items():
|
||||
new_area = crop_to.intersection(area)
|
||||
if not new_area.is_empty:
|
||||
render_data.access_restriction_affected.setdefault(access_restriction, []).append(new_area)
|
||||
|
||||
new_geoms.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_geoms.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_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.altitude for area in new_geoms.altitudeareas)
|
||||
if new_geoms.altitudeareas else new_geoms.base_altitude)
|
||||
|
||||
new_geoms.build_mesh(interpolators.get(level.pk) if sublevel.pk == level.pk else None)
|
||||
|
||||
render_data.levels.append(new_geoms)
|
||||
|
||||
render_data.access_restriction_affected = {
|
||||
access_restriction: unary_union(areas)
|
||||
for access_restriction, areas in render_data.access_restriction_affected.items()
|
||||
}
|
||||
|
||||
render_data.save(level.pk)
|
||||
|
||||
map_history.save(MapHistory.level_filename(level.pk, 'composite'))
|
||||
|
||||
cached = {}
|
||||
cache_key = None
|
||||
cache_lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def _level_filename(pk):
|
||||
return os.path.join(settings.CACHE_ROOT, 'level_%d_render_data.pickle' % pk)
|
||||
|
||||
@classmethod
|
||||
def get(cls, level):
|
||||
with cls.cache_lock:
|
||||
cache_key = MapUpdate.current_processed_cache_key()
|
||||
level_pk = str(level.pk if isinstance(level, Level) else level)
|
||||
if cls.cache_key != cache_key:
|
||||
cls.cache_key = cache_key
|
||||
cls.cached = {}
|
||||
else:
|
||||
result = cls.cached.get(level_pk, None)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
pk = level.pk if isinstance(level, Level) else level
|
||||
result = pickle.load(open(cls._level_filename(pk), 'rb'))
|
||||
|
||||
cls.cached[level_pk] = result
|
||||
return result
|
||||
|
||||
def save(self, pk):
|
||||
return pickle.dump(self, open(self._level_filename(pk), 'wb'))
|
25
src/c3nav/mapdata/render/data/mesh.py
Normal file
25
src/c3nav/mapdata/render/data/mesh.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from collections import namedtuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Mesh(namedtuple('Mesh', ('top', 'sides', 'bottom'))):
|
||||
empty_faces = np.empty((0, 3, 3)).astype(np.int32)
|
||||
|
||||
def tolist(self):
|
||||
return self.top, self.sides, self.bottom
|
||||
|
||||
def __mul__(self, other):
|
||||
return Mesh(top=np.rint(self.top*other).astype(np.int32),
|
||||
sides=np.rint(self.sides*other if other[2] != 0 else self.empty_faces),
|
||||
bottom=np.rint(self.bottom*other).astype(np.int32))
|
||||
|
||||
def __add__(self, other):
|
||||
return Mesh(np.rint(self.top+other).astype(np.int32),
|
||||
np.rint(self.sides+other).astype(np.int32),
|
||||
np.rint(self.bottom+other).astype(np.int32))
|
||||
|
||||
def filter(self, top=True, sides=True, bottom=True):
|
||||
return Mesh(top=self.top if top else self.empty_faces,
|
||||
sides=self.sides if sides else self.empty_faces,
|
||||
bottom=self.bottom if bottom else self.empty_faces)
|
Loading…
Add table
Add a link
Reference in a new issue