cache LevelRenderData for each level completely (+ cropping)

This commit is contained in:
Laura Klünder 2017-10-20 22:02:51 +02:00
parent b9c3a961af
commit 9f59b841b0
5 changed files with 223 additions and 125 deletions

View 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),
),
]

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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')