cache LevelRenderData for each level completely (+ cropping)
This commit is contained in:
parent
b9c3a961af
commit
9f59b841b0
5 changed files with 223 additions and 125 deletions
25
src/c3nav/mapdata/migrations/0038_level_render_data.py
Normal file
25
src/c3nav/mapdata/migrations/0038_level_render_data.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-10-20 19:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mapdata', '0037_level_geoms_cache'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='level',
|
||||
old_name='geoms_cache',
|
||||
new_name='render_data',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='level',
|
||||
name='render_data',
|
||||
field=models.BinaryField(null=True),
|
||||
),
|
||||
]
|
|
@ -19,7 +19,7 @@ from c3nav.mapdata.utils.svg import SVGImage
|
|||
|
||||
class LevelManager(models.Manager):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
return super().get_queryset(*args, **kwargs).defer('geoms_cache')
|
||||
return super().get_queryset(*args, **kwargs).defer('render_data')
|
||||
|
||||
|
||||
class Level(SpecificLocation, models.Model):
|
||||
|
@ -31,7 +31,7 @@ class Level(SpecificLocation, models.Model):
|
|||
on_top_of = models.ForeignKey('mapdata.Level', null=True, on_delete=models.CASCADE,
|
||||
related_name='levels_on_top', verbose_name=_('on top of'))
|
||||
|
||||
geoms_cache = models.BinaryField()
|
||||
render_data = models.BinaryField(null=True)
|
||||
|
||||
objects = LevelManager()
|
||||
|
||||
|
|
|
@ -50,9 +50,9 @@ class MapUpdate(models.Model):
|
|||
raise TypeError
|
||||
|
||||
from c3nav.mapdata.models import AltitudeArea
|
||||
from c3nav.mapdata.render.base import LevelGeometries
|
||||
from c3nav.mapdata.render.base import LevelRenderData
|
||||
AltitudeArea.recalculate()
|
||||
LevelGeometries.rebuild()
|
||||
LevelRenderData.rebuild()
|
||||
super().save(**kwargs)
|
||||
cache.set('mapdata:last_update', (self.pk, self.datetime), 900)
|
||||
delete_old_cached_tiles.apply_async(countdown=5)
|
||||
|
|
|
@ -2,19 +2,130 @@ import pickle
|
|||
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from c3nav.mapdata.models import Level, MapUpdate
|
||||
|
||||
|
||||
class AltitudeAreaGeometries:
|
||||
def __init__(self, altitudearea, colors):
|
||||
def __init__(self, altitudearea=None, colors=None):
|
||||
if altitudearea is not None:
|
||||
self.geometry = altitudearea.geometry
|
||||
self.altitude = altitudearea.altitude
|
||||
else:
|
||||
self.geometry = None
|
||||
self.altitude = None
|
||||
self.colors = colors
|
||||
|
||||
|
||||
class FakeCropper:
|
||||
@staticmethod
|
||||
def intersection(other):
|
||||
return other
|
||||
|
||||
|
||||
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__columns', 'spaces__locationgroups'))
|
||||
|
||||
single_level_geoms = {level.pk: LevelGeometries.build_for_level(level) for level in levels}
|
||||
|
||||
for i, level in enumerate(levels):
|
||||
if level.on_top_of_id is not None:
|
||||
continue
|
||||
|
||||
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(levels[:i + 1]):
|
||||
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] = crop_to if primary_level_count > 1 else FakeCropper
|
||||
|
||||
if geoms.holes is not None:
|
||||
if crop_to is None:
|
||||
crop_to = geoms.holes
|
||||
else:
|
||||
crop_to = crop_to.intersection(geoms.holes)
|
||||
|
||||
render_data = LevelRenderData()
|
||||
render_data.access_restriction_affected = {}
|
||||
|
||||
for sublevel in levels[:i + 1]:
|
||||
old_geoms = single_level_geoms[sublevel.pk]
|
||||
crop_to = level_crop_to[sublevel.pk]
|
||||
|
||||
new_geoms = LevelGeometries()
|
||||
new_geoms.doors = crop_to.intersection(old_geoms.doors)
|
||||
new_geoms.walls = crop_to.intersection(old_geoms.walls)
|
||||
|
||||
for altitudearea in old_geoms.altitudeareas:
|
||||
new_geometry = crop_to.intersection(altitudearea.geometry)
|
||||
if new_geometry.is_empty:
|
||||
continue
|
||||
|
||||
new_altitudearea = AltitudeAreaGeometries()
|
||||
new_altitudearea.geometry = new_geometry
|
||||
new_altitudearea.altitude = altitudearea.altitude
|
||||
|
||||
new_colors = {}
|
||||
for color, areas in altitudearea.colors.items():
|
||||
new_areas = {}
|
||||
for access_restriction, area in areas.items():
|
||||
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_geoms.altitudeareas.append(new_altitudearea)
|
||||
|
||||
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
|
||||
|
||||
render_data.levels.append((new_geoms, sublevel.default_height))
|
||||
|
||||
render_data.access_restriction_affected = {
|
||||
access_restriction: unary_union(areas)
|
||||
for access_restriction, areas in render_data.access_restriction_affected.items()
|
||||
}
|
||||
|
||||
level.render_data = pickle.dumps(render_data)
|
||||
|
||||
with transaction.atomic():
|
||||
for level in levels:
|
||||
level.save()
|
||||
|
||||
|
||||
class LevelGeometries:
|
||||
def __init__(self):
|
||||
self.altitudeareas = []
|
||||
|
@ -26,16 +137,7 @@ class LevelGeometries:
|
|||
self.restricted_spaces_outdoors = None
|
||||
|
||||
@staticmethod
|
||||
def crop(self, geometry, crop_to):
|
||||
if crop_to is None:
|
||||
return geometry
|
||||
return geometry.intersection(crop_to)
|
||||
|
||||
@staticmethod
|
||||
def rebuild():
|
||||
levels = Level.objects.prefetch_related('altitudeareas', 'buildings', 'doors', 'spaces',
|
||||
'spaces__holes', 'spaces__columns', 'spaces__locationgroups')
|
||||
for level in levels:
|
||||
def build_for_level(level):
|
||||
geoms = LevelGeometries()
|
||||
buildings_geom = unary_union([b.geometry for b in level.buildings.all()])
|
||||
|
||||
|
@ -114,11 +216,7 @@ class LevelGeometries:
|
|||
for access_restriction, spaces in restricted_spaces_outdoors.items()}
|
||||
|
||||
geoms.walls = buildings_geom.difference(spaces_geom).difference(doors_geom)
|
||||
level.geoms_cache = pickle.dumps(geoms)
|
||||
|
||||
with transaction.atomic():
|
||||
for level in levels:
|
||||
level.save()
|
||||
return geoms
|
||||
|
||||
|
||||
def get_level_render_data(level):
|
||||
|
@ -129,13 +227,10 @@ def get_level_render_data(level):
|
|||
return result
|
||||
|
||||
if isinstance(level, Level):
|
||||
level_pk, level_base_altitude = level.pk, level.base_altitude
|
||||
result = pickle.loads(level.render_data)
|
||||
else:
|
||||
level_pk, level_base_altitude = Level.objects.filter(pk=level).values_list('pk', 'base_altitude')[0]
|
||||
result = pickle.loads(Level.objects.filter(pk=level).values_list('render_data', flat=True)[0])
|
||||
|
||||
levels = Level.objects.filter(Q(on_top_of=level_pk) | Q(base_altitude__lte=level_base_altitude))
|
||||
result = tuple((pickle.loads(geoms_cache), default_height)
|
||||
for geoms_cache, default_height in levels.values_list('geoms_cache', 'default_height'))
|
||||
cache.set(cache_key, result, 900)
|
||||
|
||||
return result
|
||||
|
|
|
@ -29,12 +29,8 @@ class SVGRenderer:
|
|||
|
||||
@cached_property
|
||||
def affected_access_restrictions(self):
|
||||
access_restrictions = set()
|
||||
for geoms, default_height in self.level_render_data:
|
||||
for access_restriction, area in geoms.access_restriction_affected.items():
|
||||
if access_restriction not in access_restrictions and area.intersects(self.bbox):
|
||||
access_restrictions.add(access_restriction)
|
||||
return access_restrictions
|
||||
return set(ar for ar, area in self.level_render_data.access_restriction_affected.items()
|
||||
if area.intersects(self.bbox))
|
||||
|
||||
@cached_property
|
||||
def unlocked_access_restrictions(self):
|
||||
|
@ -52,28 +48,8 @@ class SVGRenderer:
|
|||
# add no access restriction to “unlocked“ access restrictions so lookup gets easier
|
||||
unlocked_access_restrictions = self.unlocked_access_restrictions | set([None])
|
||||
|
||||
# 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 geoms, default_height in reversed(self.level_render_data):
|
||||
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
|
||||
geoms.crop_to = 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)
|
||||
|
||||
for geoms, default_height in self.level_render_data:
|
||||
for geoms, default_height in self.level_render_data.levels:
|
||||
crop_to = self.bbox
|
||||
if geoms.crop_to is not None:
|
||||
crop_to = crop_to.intersection(geoms.crop_to)
|
||||
|
||||
# hide indoor and outdoor rooms if their access restriction was not unlocked
|
||||
add_walls = unary_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
|
||||
|
@ -97,9 +73,11 @@ class SVGRenderer:
|
|||
svg.add_geometry(crop_to.intersection(unary_union(areas)), fill_color=color)
|
||||
|
||||
# add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels,
|
||||
if not add_walls.is_empty or not geoms.walls.is_empty:
|
||||
svg.add_geometry(crop_to.intersection(geoms.walls.union(add_walls)),
|
||||
fill_color='#aaaaaa', stroke_px=0.5, stroke_color='#aaaaaa', elevation=default_height)
|
||||
|
||||
if not geoms.doors.is_empty:
|
||||
svg.add_geometry(crop_to.intersection(geoms.doors.difference(add_walls)),
|
||||
fill_color='#ffffff', stroke_px=0.5, stroke_color='#ffffff')
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue