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 abstract = True
base_manager_name = 'objects' 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: def get_geojson_properties(self, *args, **kwargs) -> dict:
result = OrderedDict(( result = OrderedDict((
('type', self.__class__.__name__.lower()), ('type', self.__class__.__name__.lower()),
@ -76,6 +80,14 @@ class GeometryMixin(SerializableMixin):
def recalculate_bounds(self): def recalculate_bounds(self):
self.minx, self.miny, self.maxx, self.maxy = self.geometry.bounds 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): def save(self, *args, **kwargs):
self.recalculate_bounds() self.recalculate_bounds()
super().save(*args, **kwargs) 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.access import AccessRestrictionMixin
from c3nav.mapdata.models.geometry.base import GeometryMixin from c3nav.mapdata.models.geometry.base import GeometryMixin
from c3nav.mapdata.models.locations import SpecificLocation 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 from c3nav.mapdata.utils.geometry import assert_multilinestring, assert_multipolygon, clean_geometry
@ -41,6 +42,17 @@ class LevelGeometryMixin(GeometryMixin):
result['level'] = self.level_id result['level'] = self.level_id
return result 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): 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.fields import GeometryField
from c3nav.mapdata.models.geometry.base import GeometryMixin from c3nav.mapdata.models.geometry.base import GeometryMixin
from c3nav.mapdata.models.locations import SpecificLocation from c3nav.mapdata.models.locations import SpecificLocation
from c3nav.mapdata.render.cache import changed_geometries
from c3nav.mapdata.utils.json import format_geojson from c3nav.mapdata.utils.json import format_geojson
@ -28,6 +29,21 @@ class SpaceGeometryMixin(GeometryMixin):
result['color'] = color result['color'] = color
return result 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): 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) allow_pois = models.BooleanField(_('allow pois'), db_index=True, default=True)
priority = models.IntegerField(default=0, db_index=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: class Meta:
verbose_name = _('Location Group Category') verbose_name = _('Location Group Category')
verbose_name_plural = _('Location Group Categories') verbose_name_plural = _('Location Group Categories')
@ -173,6 +177,23 @@ class LocationGroupCategory(TitledMixin, models.Model):
result.move_to_end('id', last=False) result.move_to_end('id', last=False)
return result 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): class LocationGroupManager(models.Manager):
def get_queryset(self): def get_queryset(self):
@ -193,6 +214,12 @@ class LocationGroup(Location, models.Model):
default_related_name = 'locationgroups' default_related_name = 'locationgroups'
ordering = ('-category__priority', '-priority') 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): def _serialize(self, **kwargs):
result = super()._serialize(**kwargs) result = super()._serialize(**kwargs)
result['category'] = self.category_id result['category'] = self.category_id
@ -212,6 +239,21 @@ class LocationGroup(Location, models.Model):
attributes.append(_('internal')) attributes.append(_('internal'))
return self.title + ' ('+', '.join(str(s) for s in attributes)+')' 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): class LocationRedirect(LocationSlug):
target = models.ForeignKey(LocationSlug, related_name='redirects', on_delete=models.CASCADE, target = models.ForeignKey(LocationSlug, related_name='redirects', on_delete=models.CASCADE,

View file

@ -50,8 +50,10 @@ class MapUpdate(models.Model):
raise TypeError raise TypeError
from c3nav.mapdata.models import AltitudeArea from c3nav.mapdata.models import AltitudeArea
from c3nav.mapdata.render.cache import GeometryChangeTracker
from c3nav.mapdata.render.base import LevelRenderData from c3nav.mapdata.render.base import LevelRenderData
AltitudeArea.recalculate() AltitudeArea.recalculate()
GeometryChangeTracker()
LevelRenderData.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)

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): def tile(request, level, zoom, x, y, format):
import cProfile
import pstats
pr = cProfile.Profile()
pr.enable()
zoom = int(zoom) zoom = int(zoom)
if not (0 <= zoom <= 10): if not (0 <= zoom <= 10):
raise Http404 raise Http404
@ -72,6 +77,12 @@ def tile(request, level, zoom, x, y, format):
else: else:
data = f.read() 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 = HttpResponse(data, content_type)
response['ETag'] = etag response['ETag'] = etag
response['Cache-Control'] = 'no-cache' response['Cache-Control'] = 'no-cache'