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): class LevelManager(models.Manager):
def get_queryset(self, *args, **kwargs): 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): 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, on_top_of = models.ForeignKey('mapdata.Level', null=True, on_delete=models.CASCADE,
related_name='levels_on_top', verbose_name=_('on top of')) related_name='levels_on_top', verbose_name=_('on top of'))
geoms_cache = models.BinaryField() render_data = models.BinaryField(null=True)
objects = LevelManager() objects = LevelManager()

View file

@ -50,9 +50,9 @@ class MapUpdate(models.Model):
raise TypeError raise TypeError
from c3nav.mapdata.models import AltitudeArea from c3nav.mapdata.models import AltitudeArea
from c3nav.mapdata.render.base import LevelGeometries from c3nav.mapdata.render.base import LevelRenderData
AltitudeArea.recalculate() AltitudeArea.recalculate()
LevelGeometries.rebuild() LevelRenderData.rebuild()
super().save(**kwargs) super().save(**kwargs)
cache.set('mapdata:last_update', (self.pk, self.datetime), 900) cache.set('mapdata:last_update', (self.pk, self.datetime), 900)
delete_old_cached_tiles.apply_async(countdown=5) delete_old_cached_tiles.apply_async(countdown=5)

View file

@ -2,19 +2,130 @@ import pickle
from django.core.cache import cache from django.core.cache import cache
from django.db import transaction from django.db import transaction
from django.db.models import Q
from shapely.ops import unary_union from shapely.ops import unary_union
from c3nav.mapdata.models import Level, MapUpdate from c3nav.mapdata.models import Level, MapUpdate
class AltitudeAreaGeometries: class AltitudeAreaGeometries:
def __init__(self, altitudearea, colors): def __init__(self, altitudearea=None, colors=None):
self.geometry = altitudearea.geometry if altitudearea is not None:
self.altitude = altitudearea.altitude self.geometry = altitudearea.geometry
self.altitude = altitudearea.altitude
else:
self.geometry = None
self.altitude = None
self.colors = colors 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: class LevelGeometries:
def __init__(self): def __init__(self):
self.altitudeareas = [] self.altitudeareas = []
@ -26,99 +137,86 @@ class LevelGeometries:
self.restricted_spaces_outdoors = None self.restricted_spaces_outdoors = None
@staticmethod @staticmethod
def crop(self, geometry, crop_to): def build_for_level(level):
if crop_to is None: geoms = LevelGeometries()
return geometry buildings_geom = unary_union([b.geometry for b in level.buildings.all()])
return geometry.intersection(crop_to)
@staticmethod # remove columns and holes from space areas
def rebuild(): for space in level.spaces.all():
levels = Level.objects.prefetch_related('altitudeareas', 'buildings', 'doors', 'spaces', if space.outside:
'spaces__holes', 'spaces__columns', 'spaces__locationgroups') space.geometry = space.geometry.difference(buildings_geom)
for level in levels: space.geometry = space.geometry.difference(unary_union([c.geometry for c in space.columns.all()]))
geoms = LevelGeometries() space.holes_geom = unary_union([h.geometry for h in space.holes.all()])
buildings_geom = unary_union([b.geometry for b in level.buildings.all()]) space.walkable_geom = space.geometry.difference(space.holes_geom)
# remove columns and holes from space areas spaces_geom = unary_union([s.geometry for s in level.spaces.all()])
for space in level.spaces.all(): doors_geom = unary_union([d.geometry for d in level.doors.all()])
if space.outside: walkable_spaces_geom = unary_union([s.walkable_geom for s in level.spaces.all()])
space.geometry = space.geometry.difference(buildings_geom) geoms.doors = doors_geom.difference(walkable_spaces_geom)
space.geometry = space.geometry.difference(unary_union([c.geometry for c in space.columns.all()])) walkable_geom = walkable_spaces_geom.union(geoms.doors)
space.holes_geom = unary_union([h.geometry for h in space.holes.all()]) if level.on_top_of_id is None:
space.walkable_geom = space.geometry.difference(space.holes_geom) geoms.holes = spaces_geom.difference(walkable_geom)
spaces_geom = unary_union([s.geometry for s in level.spaces.all()]) # keep track which areas are affected by access restrictions
doors_geom = unary_union([d.geometry for d in level.doors.all()]) access_restriction_affected = {}
walkable_spaces_geom = unary_union([s.walkable_geom for s in level.spaces.all()])
geoms.doors = doors_geom.difference(walkable_spaces_geom)
walkable_geom = walkable_spaces_geom.union(geoms.doors)
if level.on_top_of_id is None:
geoms.holes = spaces_geom.difference(walkable_geom)
# keep track which areas are affected by access restrictions # keep track wich spaces to hide
access_restriction_affected = {} restricted_spaces_indoors = {}
restricted_spaces_outdoors = {}
# keep track wich spaces to hide # ground colors
restricted_spaces_indoors = {} colors = {}
restricted_spaces_outdoors = {}
# ground colors # go through spaces and their areas for access control and ground colors
colors = {} for space in level.spaces.all():
access_restriction = space.access_restriction_id
if access_restriction is not None:
access_restriction_affected.setdefault(access_restriction, []).append(space.geometry)
buffered = space.geometry.buffer(0.01).union(unary_union(
tuple(door.geometry for door in level.doors.all() if door.geometry.intersects(space.geometry))
).difference(walkable_spaces_geom))
if buffered.intersects(buildings_geom):
restricted_spaces_indoors.setdefault(access_restriction, []).append(
buffered.intersection(buildings_geom)
)
if not buffered.within(buildings_geom):
restricted_spaces_outdoors.setdefault(access_restriction, []).append(
buffered.difference(buildings_geom)
)
# go through spaces and their areas for access control and ground colors colors.setdefault(space.get_color(), {}).setdefault(access_restriction, []).append(space.geometry)
for space in level.spaces.all():
access_restriction = space.access_restriction_id for area in space.areas.all():
access_restriction = area.access_restriction_id or space.access_restriction_id
if access_restriction is not None: if access_restriction is not None:
access_restriction_affected.setdefault(access_restriction, []).append(space.geometry) access_restriction_affected.setdefault(access_restriction, []).append(area.geometry)
buffered = space.geometry.buffer(0.01).union(unary_union( colors.setdefault(area.get_color(), {}).setdefault(access_restriction, []).append(area.geometry)
tuple(door.geometry for door in level.doors.all() if door.geometry.intersects(space.geometry)) colors.pop(None, None)
).difference(walkable_spaces_geom))
if buffered.intersects(buildings_geom):
restricted_spaces_indoors.setdefault(access_restriction, []).append(
buffered.intersection(buildings_geom)
)
if not buffered.within(buildings_geom):
restricted_spaces_outdoors.setdefault(access_restriction, []).append(
buffered.difference(buildings_geom)
)
colors.setdefault(space.get_color(), {}).setdefault(access_restriction, []).append(space.geometry) # merge ground colors
for color, color_group in colors.items():
for access_restriction, areas in tuple(color_group.items()):
color_group[access_restriction] = unary_union(areas)
for area in space.areas.all(): # add altitudegroup geometries and split ground colors into them
access_restriction = area.access_restriction_id or space.access_restriction_id for altitudearea in level.altitudeareas.all():
if access_restriction is not None: altitudearea_colors = {color: {access_restriction: area.intersection(altitudearea.geometry)
access_restriction_affected.setdefault(access_restriction, []).append(area.geometry) for access_restriction, area in areas.items()
colors.setdefault(area.get_color(), {}).setdefault(access_restriction, []).append(area.geometry) if area.intersects(altitudearea.geometry)}
colors.pop(None, None) for color, areas in colors.items()}
altitudearea_colors = {color: areas for color, areas in altitudearea_colors.items() if areas}
geoms.altitudeareas.append(AltitudeAreaGeometries(altitudearea, altitudearea_colors))
# merge ground colors # merge access restrictions
for color, color_group in colors.items(): geoms.access_restriction_affected = {access_restriction: unary_union(areas)
for access_restriction, areas in tuple(color_group.items()): for access_restriction, areas in access_restriction_affected.items()}
color_group[access_restriction] = unary_union(areas) geoms.restricted_spaces_indoors = {access_restriction: unary_union(spaces)
for access_restriction, spaces in restricted_spaces_indoors.items()}
geoms.restricted_spaces_outdoors = {access_restriction: unary_union(spaces)
for access_restriction, spaces in restricted_spaces_outdoors.items()}
# add altitudegroup geometries and split ground colors into them geoms.walls = buildings_geom.difference(spaces_geom).difference(doors_geom)
for altitudearea in level.altitudeareas.all(): return geoms
altitudearea_colors = {color: {access_restriction: area.intersection(altitudearea.geometry)
for access_restriction, area in areas.items()
if area.intersects(altitudearea.geometry)}
for color, areas in colors.items()}
altitudearea_colors = {color: areas for color, areas in altitudearea_colors.items() if areas}
geoms.altitudeareas.append(AltitudeAreaGeometries(altitudearea, altitudearea_colors))
# merge access restrictions
geoms.access_restriction_affected = {access_restriction: unary_union(areas)
for access_restriction, areas in access_restriction_affected.items()}
geoms.restricted_spaces_indoors = {access_restriction: unary_union(spaces)
for access_restriction, spaces in restricted_spaces_indoors.items()}
geoms.restricted_spaces_outdoors = {access_restriction: unary_union(spaces)
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()
def get_level_render_data(level): def get_level_render_data(level):
@ -129,13 +227,10 @@ def get_level_render_data(level):
return result return result
if isinstance(level, Level): if isinstance(level, Level):
level_pk, level_base_altitude = level.pk, level.base_altitude result = pickle.loads(level.render_data)
else: 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) cache.set(cache_key, result, 900)
return result return result

View file

@ -29,12 +29,8 @@ class SVGRenderer:
@cached_property @cached_property
def affected_access_restrictions(self): def affected_access_restrictions(self):
access_restrictions = set() return set(ar for ar, area in self.level_render_data.access_restriction_affected.items()
for geoms, default_height in self.level_render_data: if area.intersects(self.bbox))
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
@cached_property @cached_property
def unlocked_access_restrictions(self): def unlocked_access_restrictions(self):
@ -52,28 +48,8 @@ class SVGRenderer:
# add no access restriction to “unlocked“ access restrictions so lookup gets easier # add no access restriction to “unlocked“ access restrictions so lookup gets easier
unlocked_access_restrictions = self.unlocked_access_restrictions | set([None]) 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 for geoms, default_height in self.level_render_data.levels:
# 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:
crop_to = self.bbox 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 # 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() add_walls = unary_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
@ -97,10 +73,12 @@ class SVGRenderer:
svg.add_geometry(crop_to.intersection(unary_union(areas)), fill_color=color) 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, # add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels,
svg.add_geometry(crop_to.intersection(geoms.walls.union(add_walls)), if not add_walls.is_empty or not geoms.walls.is_empty:
fill_color='#aaaaaa', stroke_px=0.5, stroke_color='#aaaaaa', elevation=default_height) svg.add_geometry(crop_to.intersection(geoms.walls.union(add_walls)),
fill_color='#aaaaaa', stroke_px=0.5, stroke_color='#aaaaaa', elevation=default_height)
svg.add_geometry(crop_to.intersection(geoms.doors.difference(add_walls)), if not geoms.doors.is_empty:
fill_color='#ffffff', stroke_px=0.5, stroke_color='#ffffff') svg.add_geometry(crop_to.intersection(geoms.doors.difference(add_walls)),
fill_color='#ffffff', stroke_px=0.5, stroke_color='#ffffff')
return svg return svg