diff --git a/src/c3nav/mapdata/__init__.py b/src/c3nav/mapdata/__init__.py index e69de29b..e69931c9 100644 --- a/src/c3nav/mapdata/__init__.py +++ b/src/c3nav/mapdata/__init__.py @@ -0,0 +1,3 @@ + + +default_app_config = 'c3nav.mapdata.apps.MapdataConfig' diff --git a/src/c3nav/mapdata/apps.py b/src/c3nav/mapdata/apps.py new file mode 100644 index 00000000..e9400555 --- /dev/null +++ b/src/c3nav/mapdata/apps.py @@ -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() diff --git a/src/c3nav/mapdata/models/geometry/base.py b/src/c3nav/mapdata/models/geometry/base.py index 9d5dc950..501f56cb 100644 --- a/src/c3nav/mapdata/models/geometry/base.py +++ b/src/c3nav/mapdata/models/geometry/base.py @@ -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) diff --git a/src/c3nav/mapdata/models/geometry/level.py b/src/c3nav/mapdata/models/geometry/level.py index 12df8292..ae632a55 100644 --- a/src/c3nav/mapdata/models/geometry/level.py +++ b/src/c3nav/mapdata/models/geometry/level.py @@ -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): """ diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index bc070084..0360579d 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -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): """ diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 7e03b145..f9d415a0 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -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, diff --git a/src/c3nav/mapdata/models/update.py b/src/c3nav/mapdata/models/update.py index 5652359e..386c8288 100644 --- a/src/c3nav/mapdata/models/update.py +++ b/src/c3nav/mapdata/models/update.py @@ -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) diff --git a/src/c3nav/mapdata/render/cache.py b/src/c3nav/mapdata/render/cache.py new file mode 100644 index 00000000..66dd6aee --- /dev/null +++ b/src/c3nav/mapdata/render/cache.py @@ -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) diff --git a/src/c3nav/mapdata/views.py b/src/c3nav/mapdata/views.py index 9cc2404e..2b6b21f9 100644 --- a/src/c3nav/mapdata/views.py +++ b/src/c3nav/mapdata/views.py @@ -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'