register updated geometries for tile cache invalidation

This commit is contained in:
Laura Klünder 2017-10-23 19:25:15 +02:00
parent cd09cbab68
commit 5af314d282
9 changed files with 160 additions and 0 deletions

View file

@ -0,0 +1,3 @@
default_app_config = 'c3nav.mapdata.apps.MapdataConfig'

View file

@ -0,0 +1,9 @@
from django.apps import AppConfig
class MapdataConfig(AppConfig):
name = 'c3nav.mapdata'
def ready(self):
from c3nav.mapdata.render.cache import register_signals
register_signals()

View file

@ -28,6 +28,10 @@ class GeometryMixin(SerializableMixin):
abstract = True
base_manager_name = 'objects'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.orig_geometry = None if 'geometry' in self.get_deferred_fields() else self.geometry
def get_geojson_properties(self, *args, **kwargs) -> dict:
result = OrderedDict((
('type', self.__class__.__name__.lower()),
@ -76,6 +80,14 @@ class GeometryMixin(SerializableMixin):
def recalculate_bounds(self):
self.minx, self.miny, self.maxx, self.maxy = self.geometry.bounds
@property
def geometry_changed(self):
return self.orig_geometry is None or (self.geometry is not self.orig_geometry and
not self.geometry.almost_equals(self.orig_geometry, 2))
def get_changed_geometry(self):
return self.geometry if self.orig_geometry is None else self.geometry.symmetric_difference(self.orig_geometry)
def save(self, *args, **kwargs):
self.recalculate_bounds()
super().save(*args, **kwargs)

View file

@ -15,6 +15,7 @@ from c3nav.mapdata.models import Level
from c3nav.mapdata.models.access import AccessRestrictionMixin
from c3nav.mapdata.models.geometry.base import GeometryMixin
from c3nav.mapdata.models.locations import SpecificLocation
from c3nav.mapdata.render.cache import changed_geometries
from c3nav.mapdata.utils.geometry import assert_multilinestring, assert_multipolygon, clean_geometry
@ -41,6 +42,17 @@ class LevelGeometryMixin(GeometryMixin):
result['level'] = self.level_id
return result
def register_change(self, force=False):
if force or self.geometry_changed:
changed_geometries.register(self.level_id, self.geometry if force else self.get_changed_geometry())
def register_delete(self):
changed_geometries.register(self.level_id, self.geometry)
def save(self, *args, **kwargs):
self.register_change()
super().save(*args, **kwargs)
class Building(LevelGeometryMixin, models.Model):
"""

View file

@ -5,6 +5,7 @@ from shapely.geometry import CAP_STYLE, JOIN_STYLE, mapping
from c3nav.mapdata.fields import GeometryField
from c3nav.mapdata.models.geometry.base import GeometryMixin
from c3nav.mapdata.models.locations import SpecificLocation
from c3nav.mapdata.render.cache import changed_geometries
from c3nav.mapdata.utils.json import format_geojson
@ -28,6 +29,21 @@ class SpaceGeometryMixin(GeometryMixin):
result['color'] = color
return result
def register_change(self, force=True):
space = self.space
if force or self.geometry_changed:
changed_geometries.register(space.level_id, space.geometry.intersection(
self.geometry if force else self.get_changed_geometry()
))
def register_delete(self):
space = self.space
changed_geometries.register(space.level_id, space.geometry.intersection(self.geometry))
def save(self, *args, **kwargs):
self.register_change()
super().save(*args, **kwargs)
class Column(SpaceGeometryMixin, models.Model):
"""

View file

@ -160,6 +160,10 @@ class LocationGroupCategory(TitledMixin, models.Model):
allow_pois = models.BooleanField(_('allow pois'), db_index=True, default=True)
priority = models.IntegerField(default=0, db_index=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.orig_priority = self.priority
class Meta:
verbose_name = _('Location Group Category')
verbose_name_plural = _('Location Group Categories')
@ -173,6 +177,23 @@ class LocationGroupCategory(TitledMixin, models.Model):
result.move_to_end('id', last=False)
return result
def register_changed_geometries(self):
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
query = self.locationgroups.all()
for model in get_submodels(SpecificLocation):
related_name = SpecificLocation._meta.default_related_name
query.prefetch_related('locationgroup__'+related_name)
if issubclass(model, SpaceGeometryMixin):
query = query.select_related('locationgorups__'+related_name+'__space')
for group in query:
group.register_changed_geometries(do_query=False)
def save(self, *args, **kwargs):
if self.priority != self.orig_priority:
self.register_changed_geometries()
super().save(*args, **kwargs)
class LocationGroupManager(models.Manager):
def get_queryset(self):
@ -193,6 +214,12 @@ class LocationGroup(Location, models.Model):
default_related_name = 'locationgroups'
ordering = ('-category__priority', '-priority')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.orig_priority = self.priority
self.orig_category = self.category
self.orig_color = self.color
def _serialize(self, **kwargs):
result = super()._serialize(**kwargs)
result['category'] = self.category_id
@ -212,6 +239,21 @@ class LocationGroup(Location, models.Model):
attributes.append(_('internal'))
return self.title + ' ('+', '.join(str(s) for s in attributes)+')'
def register_changed_geometries(self, do_query=True):
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
for model in get_submodels(SpecificLocation):
query = getattr(self, SpecificLocation._meta.default_related_name).objects.all()
if do_query:
if issubclass(model, SpaceGeometryMixin):
query = query.select_related('space')
for obj in query:
obj.register_change(force=True)
def save(self, *args, **kwargs):
if self.orig_color != self.color or self.priority != self.orig_priority or self.category != self.orig_category:
self.register_changed_geometries()
super().save(*args, **kwargs)
class LocationRedirect(LocationSlug):
target = models.ForeignKey(LocationSlug, related_name='redirects', on_delete=models.CASCADE,

View file

@ -50,8 +50,10 @@ class MapUpdate(models.Model):
raise TypeError
from c3nav.mapdata.models import AltitudeArea
from c3nav.mapdata.render.cache import GeometryChangeTracker
from c3nav.mapdata.render.base import LevelRenderData
AltitudeArea.recalculate()
GeometryChangeTracker()
LevelRenderData.rebuild()
super().save(**kwargs)
cache.set('mapdata:last_update', (self.pk, self.datetime), 900)

View file

@ -0,0 +1,53 @@
from django.db.models.signals import m2m_changed, post_delete
from c3nav.mapdata.utils.models import get_submodels
class GeometryChangeTracker:
def __init__(self):
self._geometries_by_level = {}
self._deleted_levels = set()
def register(self, level_id, geometry):
self._geometries_by_level.setdefault(level_id, []).append(geometry)
def level_deleted(self, level_id):
self._deleted_levels.add(level_id)
def reset(self):
self._geometries_by_level = {}
self._deleted_levels = set()
changed_geometries = GeometryChangeTracker()
def geometry_deleted(sender, instance, **kwargs):
instance.register_delete()
def locationgroup_changed(sender, instance, action, reverse, model, pk_set, using, **kwargs):
if action not in ('post_add', 'post_remove', 'post_clear'):
return
if not reverse:
instance.register_change(force=True)
else:
if action not in 'post_clear':
raise NotImplementedError
query = model.objects.filter(pk__in=pk_set)
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
if issubclass(model, SpaceGeometryMixin):
query = query.select_related('space')
for obj in query:
obj.register_change(force=True)
def register_signals():
from c3nav.mapdata.models.geometry.base import GeometryMixin
for model in get_submodels(GeometryMixin):
post_delete.connect(geometry_deleted, sender=model)
from c3nav.mapdata.models.locations import SpecificLocation
for model in get_submodels(SpecificLocation):
m2m_changed.connect(locationgroup_changed, sender=model.groups.through)

View file

@ -9,6 +9,11 @@ from c3nav.mapdata.render.svg import SVGRenderer
def tile(request, level, zoom, x, y, format):
import cProfile
import pstats
pr = cProfile.Profile()
pr.enable()
zoom = int(zoom)
if not (0 <= zoom <= 10):
raise Http404
@ -72,6 +77,12 @@ def tile(request, level, zoom, x, y, format):
else:
data = f.read()
pr.disable()
s = open('/tmp/profiled', 'w')
sortby = 'cumulative'
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
response = HttpResponse(data, content_type)
response['ETag'] = etag
response['Cache-Control'] = 'no-cache'