team-3/src/c3nav/mapdata/render/renderdata.py

326 lines
15 KiB
Python
Raw Normal View History

2017-11-20 02:28:08 +01:00
import operator
import pickle
import threading
from collections import deque
from itertools import chain
2017-11-20 02:28:08 +01:00
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, Source
from c3nav.mapdata.render.geometry import AltitudeAreaGeometries, LevelGeometries
from c3nav.mapdata.utils.cache import AccessRestrictionAffected, MapHistory
from c3nav.mapdata.utils.cache.package import CachePackage
from c3nav.mapdata.utils.geometry import get_rings, unwrap_geom
2017-11-20 02:28:08 +01:00
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(unwrap_geom(geometry))
2017-11-20 02:28:08 +01:00
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:
2017-12-15 00:02:40 +01:00
"""
Renderdata for a level to display.
This contains multiple LevelGeometries instances because you might to look through holes onto lower levels.
"""
2017-11-20 02:28:08 +01:00
def __init__(self):
self.levels = []
2017-12-05 18:13:06 +01:00
self.base_altitude = None
2017-12-20 12:21:44 +01:00
self.lowest_important_level = None
self.darken_area = None
2017-11-20 02:28:08 +01:00
@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'))
package = CachePackage(bounds=tuple(chain(*Source.max_bounds())))
2017-12-15 00:02:40 +01:00
# first pass in reverse to collect some data that we need later
2017-11-20 02:28:08 +01:00
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)
2017-12-15 00:02:40 +01:00
# ignore intermediate levels in this pass
2017-11-20 02:28:08 +01:00
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
2017-12-15 00:02:40 +01:00
# create interpolator to create the pieces that fit multiple layers together
2017-11-20 02:28:08 +01:00
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))
2018-01-05 20:52:35 +01:00
if coords:
last_interpolator = NearestNDInterpolator(np.vstack(coords), np.vstack(values))
else:
2018-01-05 20:58:53 +01:00
last_interpolator = NearestNDInterpolator(np.array([[0, 0]]), np.array([float(level.base_altitude)]))
2017-11-20 02:28:08 +01:00
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
2017-12-20 12:21:44 +01:00
main_level_passed = 0
lowest_important_level = None
last_lower_bound = None
2017-11-20 02:28:08 +01:00
for sublevel in reversed(sublevels):
geoms = single_level_geoms[sublevel.pk]
if geoms.holes is not None:
primary_level_count += 1
2017-12-20 12:21:44 +01:00
# get lowest intermediate level directly below main level
if not main_level_passed:
if geoms.pk == level.pk:
main_level_passed = 1
else:
if not sublevel.on_top_of_id:
main_level_passed += 1
if main_level_passed < 2:
lowest_important_level = sublevel
# make upper bounds
if geoms.on_top_of_id is None:
if last_lower_bound is None:
geoms.upper_bound = geoms.max_altitude+geoms.max_height
else:
geoms.upper_bound = last_lower_bound
last_lower_bound = geoms.lower_bound
2017-11-20 02:28:08 +01:00
# 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()
2017-12-05 18:13:06 +01:00
render_data.base_altitude = level.base_altitude
2017-12-20 12:21:44 +01:00
render_data.lowest_important_level = lowest_important_level.pk
access_restriction_affected = {}
2017-11-20 02:28:08 +01:00
2017-12-15 00:02:40 +01:00
# go through sublevels, get their level geometries and crop them
2017-12-20 12:21:44 +01:00
lowest_important_level_passed = False
2017-11-20 02:28:08 +01:00
for sublevel in sublevels:
try:
crop_to = level_crop_to[sublevel.pk]
except KeyError:
break
old_geoms = single_level_geoms[sublevel.pk]
2017-12-20 12:21:44 +01:00
if render_data.lowest_important_level == sublevel.pk:
lowest_important_level_passed = True
if old_geoms.holes and render_data.darken_area is None and lowest_important_level_passed:
render_data.darken_area = old_geoms.holes
2017-11-20 02:28:08 +01:00
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.buildings = crop_to.intersection(old_geoms.buildings)
2018-12-05 23:15:28 +01:00
if old_geoms.on_top_of_id is None:
new_geoms.holes = crop_to.intersection(old_geoms.holes)
2017-11-20 02:28:08 +01:00
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(unwrap_geom(altitudearea.geometry))
2017-11-20 02:28:08 +01:00
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
2019-12-22 20:51:47 +01:00
new_altitudearea_obstacles = {}
for height, height_obstacles in altitudearea.obstacles.items():
new_height_obstacles = {}
for color, color_obstacles in height_obstacles.items():
new_color_obstacles = []
for obstacle in color_obstacles:
if new_geometry_prep.intersects(obstacle):
2023-11-11 13:30:45 +01:00
new_color_obstacles.append(
obstacle.intersection(unwrap_geom(altitudearea.geometry))
)
2019-12-22 20:51:47 +01:00
if new_color_obstacles:
new_height_obstacles[color] = new_color_obstacles
if new_height_obstacles:
new_altitudearea_obstacles[height] = new_height_obstacles
new_altitudearea.obstacles = new_altitudearea_obstacles
2017-11-20 02:28:08 +01:00
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(unwrap_geom(ramp)) for ramp in old_geoms.ramps)
2017-11-20 02:28:08 +01:00
if not ramp.is_empty
)
new_geoms.heightareas = tuple(
(area, height) for area, height in ((crop_to.intersection(unwrap_geom(area)), height)
2017-11-20 02:28:08 +01:00
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.access_restriction_affected.items():
2017-11-20 02:28:08 +01:00
new_area = crop_to.intersection(area)
if not new_area.is_empty:
access_restriction_affected.setdefault(access_restriction, []).append(new_area)
2017-11-20 02:28:08 +01:00
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.max_altitude = (max(area.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
2017-11-20 02:28:08 +01:00
new_geoms.build_mesh(interpolators.get(level.pk) if sublevel.pk == level.pk else None)
render_data.levels.append(new_geoms)
access_restriction_affected = {
2017-11-20 02:28:08 +01:00
access_restriction: unary_union(areas)
for access_restriction, areas in access_restriction_affected.items()
2017-11-20 02:28:08 +01:00
}
access_restriction_affected = AccessRestrictionAffected.build(access_restriction_affected)
access_restriction_affected.save_level(level.pk, 'composite')
map_history.save_level(level.pk, 'composite')
package.add_level(level.pk, map_history, access_restriction_affected)
2017-11-20 02:28:08 +01:00
render_data.save(level.pk)
package.save_all()
2017-11-20 02:28:08 +01:00
cached = {}
cache_key = None
cache_lock = threading.Lock()
@staticmethod
def _level_filename(pk):
return settings.CACHE_ROOT / ('render_data_level_%d.pickle' % pk)
2017-11-20 02:28:08 +01:00
@classmethod
def get(cls, level):
2017-12-15 00:02:40 +01:00
# get the current render data from local variable if no new processed mapupdate exists.
# this is much faster than any other possible cache
2017-11-20 02:28:08 +01:00
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'))