From b309b6f6cd4c3b776d814c74092095716f3bb5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 1 Dec 2016 12:25:02 +0100 Subject: [PATCH] add LevelConnector --- src/c3nav/editor/forms.py | 13 ++- src/c3nav/editor/static/editor/js/editor.js | 2 + src/c3nav/editor/views.py | 25 +++-- src/c3nav/mapdata/api.py | 7 +- .../mapdata/migrations/0009_levelconnector.py | 32 +++++++ src/c3nav/mapdata/models/__init__.py | 2 +- src/c3nav/mapdata/models/geometry.py | 93 ++++++++++++++++--- src/c3nav/mapdata/models/level.py | 3 + src/c3nav/mapdata/packageio/const.py | 4 +- src/c3nav/mapdata/packageio/read.py | 11 +++ 10 files changed, 164 insertions(+), 28 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0009_levelconnector.py diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index ed99bcd8..c3644d12 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -6,6 +6,7 @@ from django.conf import settings from django.forms import CharField, ModelForm, ValidationError from django.forms.models import ModelChoiceField from django.forms.widgets import HiddenInput +from django.utils.translation import ugettext_lazy as _ from shapely.geometry.geo import mapping from c3nav.mapdata.models import Package @@ -50,6 +51,10 @@ class MapitemFormMixin(ModelForm): if not creating: self.initial['level'] = self.instance.level.name + if 'levels' in self.fields: + # set field_name + self.fields['levels'].to_field_name = 'name' + if 'geometry' in self.fields: # hide geometry widget self.fields['geometry'].widget = HiddenInput() @@ -73,6 +78,12 @@ class MapitemFormMixin(ModelForm): initial=titles[language].strip(), max_length=50) self.titles = titles + def clean_levels(self): + levels = self.cleaned_data.get('levels') + if len(levels) < 2: + raise ValidationError(_('Please select at least two levels.')) + return levels + def clean(self): if 'geometry' in self.fields: if not self.cleaned_data.get('geometry'): @@ -80,7 +91,7 @@ class MapitemFormMixin(ModelForm): def create_editor_form(mapitemtype): - possible_fields = ['name', 'package', 'level', 'geometry', 'height', 'elevator', 'button'] + possible_fields = ['name', 'package', 'level', 'levels', 'geometry', 'height', 'elevator', 'button'] existing_fields = [field for field in possible_fields if hasattr(mapitemtype, field)] class EditorForm(MapitemFormMixin, ModelForm): diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index 6f61cd97..9667d273 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -160,6 +160,7 @@ editor = { 'door': '#FF00FF', 'hole': '#66CC66', 'elevatorlevel': '#9EF8FB', + 'levelconnector': '#FFFF00' }, _get_geometry_style: function (feature) { // style callback for GeoJSON loader @@ -281,6 +282,7 @@ editor = { } editor._creating = true; $('#id_level').val(editor._level); + $('#id_levels').find('option[value='+editor._level+']').prop('selected', true); } } else if (editor._get_geometries_next_time) { editor.get_geometries(); diff --git a/src/c3nav/editor/views.py b/src/c3nav/editor/views.py index 75228ba0..c50af845 100644 --- a/src/c3nav/editor/views.py +++ b/src/c3nav/editor/views.py @@ -15,9 +15,13 @@ from c3nav.mapdata.permissions import can_access_package, filter_queryset_by_pac def list_mapitemtypes(request, level): def get_item_count(mapitemtype): - if not hasattr(mapitemtype, 'level'): - return 0 - return filter_queryset_by_package_access(request, mapitemtype.objects.filter(level__name=level)).count() + if hasattr(mapitemtype, 'level'): + return filter_queryset_by_package_access(request, mapitemtype.objects.filter(level__name=level)).count() + + if hasattr(mapitemtype, 'levels'): + return filter_queryset_by_package_access(request, mapitemtype.objects.filter(levels__name=level)).count() + + return 0 return render(request, 'editor/mapitemtypes.html', { 'level': level, @@ -25,7 +29,7 @@ def list_mapitemtypes(request, level): { 'name': name, 'title': mapitemtype._meta.verbose_name_plural, - 'has_level': hasattr(mapitemtype, 'level'), + 'has_level': hasattr(mapitemtype, 'level') or hasattr(mapitemtype, 'levels'), 'count': get_item_count(mapitemtype), } for name, mapitemtype in MAPITEM_TYPES.items() ], @@ -37,14 +41,18 @@ def list_mapitems(request, mapitem_type, level=None): if mapitemtype is None: raise Http404('Unknown mapitemtype.') - if hasattr(mapitemtype, 'level') and level is None: + has_level = hasattr(mapitemtype, 'level') or hasattr(mapitemtype, 'levels') + if has_level and level is None: raise Http404('Missing level.') - elif not hasattr(mapitemtype, 'level') and level is not None: + elif not has_level and level is not None: return redirect('editor.mapitems', mapitem_type=mapitem_type) queryset = mapitemtype.objects.all() if level is not None: - queryset = queryset.filter(level__name=level) + if hasattr(mapitemtype, 'level'): + queryset = queryset.filter(level__name=level) + elif hasattr(mapitemtype, 'levels'): + queryset = queryset.filter(levels__name=level) return render(request, 'editor/mapitems.html', { 'mapitem_type': mapitem_type, @@ -105,7 +113,7 @@ def edit_mapitem(request, mapitem_type, name=None): # Update/create mapitem commit_type = 'Created' if mapitem is None else 'Updated' action = 'create' if mapitem is None else 'edit' - mapitem = form.instance + mapitem = form.save(commit=False) if form.titles is not None: mapitem.titles = {} @@ -130,6 +138,7 @@ def edit_mapitem(request, mapitem_type, name=None): }) mapitem.save() + form.save_m2m() return render(request, 'editor/mapitem_success.html', { 'mapitem_type': mapitem_type diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 8ba217f1..ec5c0f78 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -38,7 +38,12 @@ class GeometryViewSet(ViewSet): if packages: queryset = queryset.filter(package__name__in=packages) if levels: - queryset = queryset.filter(level__name__in=levels) + if hasattr(mapitemtype, 'level'): + queryset = queryset.filter(level__name__in=levels) + elif hasattr(mapitemtype, 'levels'): + queryset = queryset.filter(levels__name__in=levels) + else: + queryset = queryset.none() if names: queryset = queryset.filter(name__in=names) queryset = filter_queryset_by_package_access(request, queryset) diff --git a/src/c3nav/mapdata/migrations/0009_levelconnector.py b/src/c3nav/mapdata/migrations/0009_levelconnector.py new file mode 100644 index 00000000..ec88c7b2 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0009_levelconnector.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-01 10:48 +from __future__ import unicode_literals + +import c3nav.mapdata.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0008_hole'), + ] + + operations = [ + migrations.CreateModel( + name='LevelConnector', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(unique=True, verbose_name='Name')), + ('geometry', c3nav.mapdata.fields.GeometryField()), + ('levels', models.ManyToManyField(related_name='levelconnectors', to='mapdata.Level', verbose_name='levels')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levelconnectors', to='mapdata.Package', verbose_name='map package')), + ], + options={ + 'default_related_name': 'levelconnectors', + 'verbose_name_plural': 'Level Connectors', + 'verbose_name': 'Level Connector', + }, + ), + ] diff --git a/src/c3nav/mapdata/models/__init__.py b/src/c3nav/mapdata/models/__init__.py index 575317d0..0b53072e 100644 --- a/src/c3nav/mapdata/models/__init__.py +++ b/src/c3nav/mapdata/models/__init__.py @@ -2,4 +2,4 @@ from .level import Level # noqa from .package import Package # noqa from .source import Source # noqa from .collections import Elevator # noqa -from .geometry import GeometryMapItem, GEOMETRY_MAPITEM_TYPES # noqa +from .geometry import GeometryMapItemWithLevel, GEOMETRY_MAPITEM_TYPES # noqa diff --git a/src/c3nav/mapdata/models/geometry.py b/src/c3nav/mapdata/models/geometry.py index 5cf031a5..5f9d8154 100644 --- a/src/c3nav/mapdata/models/geometry.py +++ b/src/c3nav/mapdata/models/geometry.py @@ -24,7 +24,6 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta): """ A map feature """ - level = models.ForeignKey('mapdata.Level', on_delete=models.CASCADE, verbose_name=_('level')) geometry = GeometryField() geomtype = None @@ -47,10 +46,6 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta): except: raise ValueError(_('Invalid GeoJSON.')) - if 'level' not in data: - raise ValueError('missing level.') - kwargs['level'] = data['level'] - return kwargs @classmethod @@ -64,7 +59,6 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta): ('type', self.__class__.__name__.lower()), ('name', self.name), ('package', self.package.name), - ('level', self.level.name), )) def to_geojson(self): @@ -76,12 +70,81 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta): def tofile(self): result = super().tofile() - result['level'] = self.level.name result['geometry'] = format_geojson(mapping(self.geometry)) return result -class Building(GeometryMapItem): +class GeometryMapItemWithLevel(GeometryMapItem): + """ + A map feature + """ + level = models.ForeignKey('mapdata.Level', on_delete=models.CASCADE, verbose_name=_('level')) + + class Meta: + abstract = True + + @classmethod + def fromfile(cls, data, file_path): + kwargs = super().fromfile(data, file_path) + + if 'level' not in data: + raise ValueError('missing level.') + kwargs['level'] = data['level'] + + return kwargs + + def get_geojson_properties(self): + result = super().get_geojson_properties() + result['level'] = float(self.level.name) + return result + + def tofile(self): + result = super().tofile() + result['level'] = self.level.name + result.move_to_end('geometry') + return result + + +class LevelConnector(GeometryMapItem): + """ + A connector connecting levels + """ + geomtype = 'polygon' + levels = models.ManyToManyField('mapdata.Level', verbose_name=_('levels')) + + class Meta: + verbose_name = _('Level Connector') + verbose_name_plural = _('Level Connectors') + default_related_name = 'levelconnectors' + + @classmethod + def fromfile(cls, data, file_path): + kwargs = super().fromfile(data, file_path) + + if 'levels' not in data: + raise ValueError('missing levels.') + levels = data.get('levels', None) + if not isinstance(levels, list): + raise TypeError('levels has to be a list') + if len(levels) < 2: + raise ValueError('a level connector needs at least two levels') + kwargs['levels'] = levels + + return kwargs + + def get_geojson_properties(self): + result = super().get_geojson_properties() + result['levels'] = tuple(self.levels.all().order_by('name').values_list('name', flat=True)) + return result + + def tofile(self): + result = super().tofile() + result['levels'] = tuple(self.levels.all().order_by('name').values_list('name', flat=True)) + result.move_to_end('geometry') + return result + + +class Building(GeometryMapItemWithLevel): """ The outline of a building on a specific level """ @@ -93,7 +156,7 @@ class Building(GeometryMapItem): default_related_name = 'buildings' -class Room(GeometryMapItem): +class Room(GeometryMapItemWithLevel): """ An accessible area like a room. Can overlap. """ @@ -105,7 +168,7 @@ class Room(GeometryMapItem): default_related_name = 'rooms' -class Outside(GeometryMapItem): +class Outside(GeometryMapItemWithLevel): """ An accessible outdoor area like a court. Can overlap. """ @@ -117,7 +180,7 @@ class Outside(GeometryMapItem): default_related_name = 'outsides' -class Obstacle(GeometryMapItem): +class Obstacle(GeometryMapItemWithLevel): """ An obstacle """ @@ -153,7 +216,7 @@ class Obstacle(GeometryMapItem): return result -class Door(GeometryMapItem): +class Door(GeometryMapItemWithLevel): """ A connection between two rooms """ @@ -165,7 +228,7 @@ class Door(GeometryMapItem): default_related_name = 'doors' -class Hole(GeometryMapItem): +class Hole(GeometryMapItemWithLevel): """ A hole in the ground of a room, e.g. for stairs. """ @@ -177,11 +240,11 @@ class Hole(GeometryMapItem): default_related_name = 'holes' -class ElevatorLevel(GeometryMapItem): +class ElevatorLevel(GeometryMapItemWithLevel): """ An elevator Level """ - elevator = models.ForeignKey(Elevator, on_delete=models.PROTECT, related_name='levels') + elevator = models.ForeignKey(Elevator, on_delete=models.PROTECT) button = models.SlugField(_('Button label'), max_length=10) geomtype = 'polygon' diff --git a/src/c3nav/mapdata/models/level.py b/src/c3nav/mapdata/models/level.py index 63fc7396..2724c1e3 100644 --- a/src/c3nav/mapdata/models/level.py +++ b/src/c3nav/mapdata/models/level.py @@ -46,6 +46,9 @@ class Level(MapItem): result['altitude'] = float(self.altitude) return result + def __str__(self): + return self.name + class LevelGeometries(): def __init__(self, level): diff --git a/src/c3nav/mapdata/packageio/const.py b/src/c3nav/mapdata/packageio/const.py index 13a779cb..f65b78e6 100644 --- a/src/c3nav/mapdata/packageio/const.py +++ b/src/c3nav/mapdata/packageio/const.py @@ -1,6 +1,6 @@ from c3nav.mapdata.models import Level, Package, Source from c3nav.mapdata.models.collections import Elevator -from c3nav.mapdata.models.geometry import Building, Door, ElevatorLevel, Hole, Obstacle, Outside, Room +from c3nav.mapdata.models.geometry import Building, Door, ElevatorLevel, Hole, LevelConnector, Obstacle, Outside, Room -ordered_models = (Package, Level, Source, Building, Room, Outside, Door, Obstacle, Hole) +ordered_models = (Package, Level, LevelConnector, Source, Building, Room, Outside, Door, Obstacle, Hole) ordered_models += (Elevator, ElevatorLevel) diff --git a/src/c3nav/mapdata/packageio/read.py b/src/c3nav/mapdata/packageio/read.py index a8d54e4a..7ec666f8 100644 --- a/src/c3nav/mapdata/packageio/read.py +++ b/src/c3nav/mapdata/packageio/read.py @@ -8,6 +8,7 @@ from django.conf import settings from django.core.management import CommandError from c3nav.mapdata.models import Elevator, Level, Package +from c3nav.mapdata.models.geometry import LevelConnector from c3nav.mapdata.packageio.const import ordered_models @@ -158,6 +159,11 @@ class ReaderItem: depends = [self.reader.saved_items[Package][name].obj.pk for name in self.data['depends']] self.data.pop('depends') + levels = [] + if self.model == LevelConnector: + levels = [self.reader.saved_items[Level][name].obj.pk for name in self.data['levels']] + self.data.pop('levels') + # Change name references to the referenced object for name, model in self.relations.items(): if name in self.data: @@ -174,3 +180,8 @@ class ReaderItem: self.obj.depends.clear() for dependency in depends: self.obj.depends.add(dependency) + + if levels: + self.obj.levels.clear() + for level in levels: + self.obj.levels.add(level)