From 9910b3ed83514a0c0e78ee1dfd15fdb4fe392a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 10 Jun 2017 14:58:13 +0200 Subject: [PATCH] replace Space.level with secondary sections on top of primary sections --- src/c3nav/editor/api.py | 91 ++++++++++++------- src/c3nav/editor/forms.py | 2 +- src/c3nav/editor/views.py | 4 +- src/c3nav/mapdata/api.py | 12 +++ .../mapdata/migrations/0010_on_top_of.py | 40 ++++++++ src/c3nav/mapdata/models/geometry/section.py | 10 +- src/c3nav/mapdata/models/section.py | 10 +- 7 files changed, 122 insertions(+), 47 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0010_on_top_of.py diff --git a/src/c3nav/editor/api.py b/src/c3nav/editor/api.py index 86311496..430688c9 100644 --- a/src/c3nav/editor/api.py +++ b/src/c3nav/editor/api.py @@ -16,6 +16,41 @@ class EditorViewSet(ViewSet): /geometries/ returns a list of geojson features, you have to specify ?section= or ?space= /geometrystyles/ returns styling information for all geometry types """ + def _get_section_geometries(self, section: Section): + buildings = section.buildings.all() + buildings_geom = cascaded_union([building.geometry for building in buildings]) + spaces = {space.id: space for space in section.spaces.all()} + holes_geom = [] + for space in spaces.values(): + if space.outside: + space.geometry = space.geometry.difference(buildings_geom) + else: + space.geometry = space.geometry.intersection(buildings_geom) + columns_geom = cascaded_union([column.geometry for column in space.columns.all()]) + space.geometry = space.geometry.difference(columns_geom) + space_holes_geom = cascaded_union([hole.geometry for hole in space.holes.all()]) + holes_geom.append(space_holes_geom.intersection(space.geometry)) + space.geometry = space.geometry.difference(space_holes_geom) + holes_geom = cascaded_union(holes_geom) + + for building in buildings: + building.original_geometry = building.geometry + for obj in chain(buildings, spaces.values()): + obj.geometry = obj.geometry.difference(holes_geom) + + results = [] + results.extend(buildings) + for door in section.doors.all(): + results.append(door) + + results.extend(spaces.values()) + areas = Area.objects.filter(space__in=spaces.values()).prefetch_related('groups') + areas = [area for area in areas if area.get_color()] + for area in areas: + area.geometry = area.geometry.intersection(spaces[area.space_id].geometry) + results.extend((area for area in areas if not area.geometry.is_empty)) + return results + @list_route(methods=['get']) def geometries(self, request, *args, **kwargs): section = request.GET.get('section') @@ -24,45 +59,35 @@ class EditorViewSet(ViewSet): if space is not None: raise ValidationError('Only section or space can be specified.') section = get_object_or_404(Section, pk=section) - buildings = section.buildings.all() - buildings_geom = cascaded_union([building.geometry for building in buildings]) - spaces = {space.id: space for space in section.spaces.all().prefetch_related('groups', 'holes', 'columns')} - holes_geom = [] - for space in spaces.values(): - if space.outside: - space.geometry = space.geometry.difference(buildings_geom) - else: - space.geometry = space.geometry.intersection(buildings_geom) - columns_geom = cascaded_union([column.geometry for column in space.columns.all()]) - space.geometry = space.geometry.difference(columns_geom) - space_holes_geom = cascaded_union([hole.geometry for hole in space.holes.all()]) - holes_geom.append(space_holes_geom.intersection(space.geometry)) - space.geometry = space.geometry.difference(space_holes_geom) - holes_geom = cascaded_union(holes_geom) - for building in buildings: - building.original_geometry = building.geometry - for obj in chain(buildings, (s for s in spaces.values() if s.level == 'normal')): - obj.geometry = obj.geometry.difference(holes_geom) + sections_under = () + sections_on_top = () - results = [] + lower_section = section.lower().first() + primary_sections = (section, ) + ((lower_section, ) if lower_section else ()) + secondary_sections = Section.objects.filter(on_top_of__in=primary_sections).values_list('pk', 'on_top_of') + if lower_section: + sections_under = tuple(pk for pk, on_top_of in secondary_sections if on_top_of == lower_section.pk) - def add_spaces(level): - results.extend(space for space in spaces.values() if space.level == level) - areas = Area.objects.filter(space__section=section, space__level=level).prefetch_related('groups') - areas = [area for area in areas if area.get_color()] - for area in areas: - area.geometry = area.geometry.intersection(spaces[area.space_id].geometry) - results.extend((area for area in areas if not area.geometry.is_empty)) + if True: + sections_on_top = tuple(pk for pk, on_top_of in secondary_sections if on_top_of == section.pk) - add_spaces('lower') + sections = chain([section.pk], sections_under, sections_on_top) + sections = Section.objects.filter(pk__in=sections).prefetch_related('buildings', 'spaces', 'doors', + 'spaces__groups', 'spaces__holes', + 'spaces__columns') + sections = {s.pk: s for s in sections} - results.extend(buildings) - for door in section.doors.all(): - results.append(door) + section = sections[section.pk] + sections_under = [sections[pk] for pk in sections_under] + sections_on_top = [sections[pk] for pk in sections_on_top] + + results = chain( + *(self._get_section_geometries(s) for s in sections_under), + self._get_section_geometries(section), + *(self._get_section_geometries(s) for s in sections_on_top) + ) - add_spaces('normal') - add_spaces('upper') return Response([obj.to_geojson() for obj in results]) elif space is not None: space = get_object_or_404(Space.objects.select_related('section'), pk=space) diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index e12a3655..8fd63676 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -89,7 +89,7 @@ class MapitemFormMixin(ModelForm): def create_editor_form(editor_model): - possible_fields = ['slug', 'name', 'altitude', 'level', 'category', 'width', 'groups', 'color', 'public', + possible_fields = ['slug', 'name', 'altitude', 'category', 'width', 'groups', 'color', 'public', 'can_search', 'can_describe', 'outside', 'stuffed', 'geometry', 'left', 'top', 'right', 'bottom'] field_names = [field.name for field in editor_model._meta.get_fields()] diff --git a/src/c3nav/editor/views.py b/src/c3nav/editor/views.py index 15ef244a..dbbc3604 100644 --- a/src/c3nav/editor/views.py +++ b/src/c3nav/editor/views.py @@ -39,7 +39,7 @@ def child_model(model_name, kwargs=None, parent=None): @sidebar_view def main_index(request): return render(request, 'editor/index.html', { - 'sections': Section.objects.all(), + 'sections': Section.objects.filter(on_top_of__isnull=True), 'child_models': [ child_model('LocationGroup'), child_model('Source'), @@ -52,7 +52,7 @@ def section_detail(request, pk): section = get_object_or_404(Section, pk=pk) return render(request, 'editor/section.html', { - 'sections': Section.objects.all(), + 'sections': Section.objects.filter(on_top_of__isnull=True), 'section': section, 'section_url': 'editor.sections.detail', 'section_as_pk': True, diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 94f4b930..361f8935 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -38,6 +38,17 @@ class MapdataViewSet(ReadOnlyModelViewSet): except Space.DoesNotExist: raise NotFound(detail=_('space not found.')) qs = qs.filter(space=space) + if qs.model == Section and 'on_top_of' in request.GET: + if request.GET['on_top_of'] == 'null': + qs = qs.filter(on_top_of__isnull=False) + else: + if not request.GET['on_top_of'].isdigit(): + raise ValidationError(detail={'detail': _('%s is not null or an integer.') % 'on_top_of'}) + try: + section = Section.objects.get(pk=request.GET['on_top_of']) + except Section.DoesNotExist: + raise NotFound(detail=_('section not found.')) + qs = qs.filter(on_top_of=section) return Response([obj.serialize(geometry=geometry) for obj in qs.order_by('id')]) def retrieve(self, request, *args, **kwargs): @@ -51,6 +62,7 @@ class MapdataViewSet(ReadOnlyModelViewSet): class SectionViewSet(MapdataViewSet): + """ Add ?on_top_of=null or ?on_top_of= to filter by on_top_of. """ queryset = Section.objects.all() @list_route(methods=['get']) diff --git a/src/c3nav/mapdata/migrations/0010_on_top_of.py b/src/c3nav/mapdata/migrations/0010_on_top_of.py new file mode 100644 index 00000000..7d0a373f --- /dev/null +++ b/src/c3nav/mapdata/migrations/0010_on_top_of.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2017-06-10 11:15 +from __future__ import unicode_literals + +from decimal import Decimal +from django.db import migrations, models +import django.db.models.deletion + + +def move_upper_spaces_to_section_on_top(apps, schema_editor): + Section = apps.get_model('mapdata', 'Section') + Space = apps.get_model('mapdata', 'Space') + Space.objects.filter(level='lower').delete() + for section in Section.objects.all(): + if Space.objects.filter(level='upper', section=section).exists(): + section_on_top_of = section.sections_on_top.create(altitude=section.altitude+Decimal('0.01'), public=section.public, + can_search=False, can_describe=False) + Space.objects.filter(level='upper', section=section).update(section=section_on_top_of, outside=True) + + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0009_column'), + ] + + operations = [ + migrations.AddField( + model_name='section', + name='on_top_of', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='sections_on_top', to='mapdata.Section', verbose_name='on top of'), + ), + migrations.RunPython(move_upper_spaces_to_section_on_top), + migrations.RemoveField( + model_name='space', + name='level', + ), + ] diff --git a/src/c3nav/mapdata/models/geometry/section.py b/src/c3nav/mapdata/models/geometry/section.py index da655f58..4b68e662 100644 --- a/src/c3nav/mapdata/models/geometry/section.py +++ b/src/c3nav/mapdata/models/geometry/section.py @@ -16,7 +16,6 @@ class SectionGeometryMixin(GeometryMixin): def get_geojson_properties(self) -> dict: result = super().get_geojson_properties() - result['layer'] = getattr(self, 'level', 'base') if hasattr(self, 'get_color'): color = self.get_color() if color: @@ -44,13 +43,8 @@ class Building(SectionGeometryMixin, models.Model): class Space(SpecificLocation, SectionGeometryMixin, models.Model): """ - An accessible space. Shouldn't overlap with spaces on same secion and level. + An accessible space. Shouldn't overlap with spaces on same section. """ - LEVELS = ( - ('normal', _('normal')), - ('upper', _('upper')), - ('lower', _('lower')), - ) CATEGORIES = ( ('normal', _('normal')), ('stairs', _('stairs')), @@ -58,7 +52,6 @@ class Space(SpecificLocation, SectionGeometryMixin, models.Model): ('elevator', _('elevator')), ) geometry = GeometryField('polygon') - level = models.CharField(verbose_name=_('level'), choices=LEVELS, default='normal', max_length=16) category = models.CharField(verbose_name=_('category'), choices=CATEGORIES, default='normal', max_length=16) outside = models.BooleanField(default=False, verbose_name=_('is outside of building')) @@ -71,7 +64,6 @@ class Space(SpecificLocation, SectionGeometryMixin, models.Model): result = super()._serialize(**kwargs) if space: result['category'] = self.category - result['level'] = self.level result['public'] = self.public return result diff --git a/src/c3nav/mapdata/models/section.py b/src/c3nav/mapdata/models/section.py index ab90ace4..a641d153 100644 --- a/src/c3nav/mapdata/models/section.py +++ b/src/c3nav/mapdata/models/section.py @@ -14,6 +14,8 @@ class Section(SpecificLocation, EditorFormMixin, models.Model): A map section like a level """ altitude = models.DecimalField(_('section altitude'), null=False, unique=True, max_digits=6, decimal_places=2) + on_top_of = models.ForeignKey('mapdata.Section', null=True, on_delete=models.CASCADE, + related_name='sections_on_top', verbose_name=_('on top of')) class Meta: verbose_name = _('Section') @@ -25,10 +27,14 @@ class Section(SpecificLocation, EditorFormMixin, models.Model): super().__init__(*args, **kwargs) def lower(self): - return Section.objects.filter(altitude__lt=self.altitude).order_by('altitude') + if self.on_top_of is not None: + raise TypeError + return Section.objects.filter(altitude__lt=self.altitude, on_top_of__isnull=True).order_by('-altitude') def higher(self): - return Section.objects.filter(altitude__gt=self.altitude).order_by('altitude') + if self.on_top_of is not None: + raise TypeError + return Section.objects.filter(altitude__gt=self.altitude, on_top_of__isnull=True).order_by('altitude') def _serialize(self, section=True, **kwargs): result = super()._serialize(**kwargs)