diff --git a/src/c3nav/api/urls.py b/src/c3nav/api/urls.py index 51f26eb7..81f228f4 100644 --- a/src/c3nav/api/urls.py +++ b/src/c3nav/api/urls.py @@ -15,7 +15,7 @@ router.register(r'levels', LevelViewSet) router.register(r'packages', PackageViewSet) router.register(r'sources', SourceViewSet) router.register(r'featuretypes', FeatureTypeViewSet, base_name='featuretype') -router.register(r'features', FeatureViewSet) +router.register(r'features', FeatureViewSet, base_name='feature') router.register(r'hosters', HosterViewSet, base_name='hoster') router.register(r'submittasks', SubmitTaskViewSet, base_name='submittask') diff --git a/src/c3nav/editor/views.py b/src/c3nav/editor/views.py index fde66f6f..1176014c 100644 --- a/src/c3nav/editor/views.py +++ b/src/c3nav/editor/views.py @@ -5,9 +5,9 @@ from django.core.signing import BadSignature from django.http.response import Http404 from django.shortcuts import get_object_or_404, render -from c3nav.editor.forms import FeatureForm +# from c3nav.editor.forms import FeatureForm from c3nav.editor.hosters import get_hoster_for_package, hosters -from c3nav.mapdata.models.feature import FEATURE_TYPES, Feature +from c3nav.mapdata.models.features import FEATURE_TYPES, Feature from c3nav.mapdata.models.package import Package from c3nav.mapdata.packageio.write import json_encode from c3nav.mapdata.permissions import can_access_package diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 75354d54..a688c614 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -1,5 +1,6 @@ import mimetypes import os +from itertools import chain from django.conf import settings from django.core.files import File @@ -8,7 +9,8 @@ from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet -from c3nav.mapdata.models import FEATURE_TYPES, Feature, Level, Package, Source +from c3nav.mapdata.models import FEATURE_TYPES, Level, Package, Source +from c3nav.mapdata.models.features import Feature from c3nav.mapdata.permissions import filter_source_queryset from c3nav.mapdata.serializers import (FeatureSerializer, FeatureTypeSerializer, LevelSerializer, PackageSerializer, SourceSerializer) @@ -89,7 +91,15 @@ class FeatureViewSet(ReadOnlyModelViewSet): """ List and retrieve map features you have access to """ - queryset = Feature.objects.all() + model = Feature + base_name = 'feature' serializer_class = FeatureSerializer lookup_field = 'name' lookup_value_regex = '[^/]+' + + def get_queryset(self): + querysets = [] + for name, model in FEATURE_TYPES.items(): + querysets.append(model.objects.all()) + return chain(*querysets) + diff --git a/src/c3nav/mapdata/migrations/0001_initial.py b/src/c3nav/mapdata/migrations/0001_initial.py index b427d5e4..7c3b04dc 100644 --- a/src/c3nav/mapdata/migrations/0001_initial.py +++ b/src/c3nav/mapdata/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.1 on 2016-10-11 14:00 +# Generated by Django 1.10.1 on 2016-10-12 12:12 from __future__ import unicode_literals import c3nav.mapdata.fields @@ -16,14 +16,17 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Feature', + name='Inside', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.SlugField(unique=True, verbose_name='feature identifier')), - ('feature_type', models.CharField(choices=[('building', 'Building'), ('room', 'Room'), ('outside', 'Outside Area'), ('obstacle', 'Obstacle')], max_length=50)), - ('titles', c3nav.mapdata.fields.JSONField()), + ('name', models.SlugField(unique=True, verbose_name='name')), ('geometry', c3nav.mapdata.fields.GeometryField()), ], + options={ + 'default_related_name': 'insides', + 'verbose_name': 'Inside Area', + 'verbose_name_plural': 'Inside Areas', + }, ), migrations.CreateModel( name='Level', @@ -32,6 +35,9 @@ class Migration(migrations.Migration): ('name', models.SlugField(help_text='Usually just an integer (e.g. -1, 0, 1, 2)', unique=True, verbose_name='level name')), ('altitude', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='level altitude')), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='Package', @@ -47,6 +53,24 @@ class Migration(migrations.Migration): ('directory', models.CharField(max_length=100, verbose_name='folder name')), ('depends', models.ManyToManyField(to='mapdata.Package')), ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Room', + 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()), + ('level', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='mapdata.Level', verbose_name='level')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='mapdata.Package', verbose_name='map package')), + ], + options={ + 'default_related_name': 'rooms', + 'verbose_name': 'Room', + 'verbose_name_plural': 'Rooms', + }, ), migrations.CreateModel( name='Source', @@ -59,6 +83,9 @@ class Migration(migrations.Migration): ('right', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='right coordinate')), ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sources', to='mapdata.Package', verbose_name='map package')), ], + options={ + 'abstract': False, + }, ), migrations.AddField( model_name='level', @@ -66,13 +93,13 @@ class Migration(migrations.Migration): field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='mapdata.Package', verbose_name='map package'), ), migrations.AddField( - model_name='feature', + model_name='inside', name='level', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='mapdata.Level', verbose_name='level'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='insides', to='mapdata.Level', verbose_name='level'), ), migrations.AddField( - model_name='feature', + model_name='inside', name='package', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='mapdata.Package', verbose_name='map package'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='insides', to='mapdata.Package', verbose_name='map package'), ), ] diff --git a/src/c3nav/mapdata/models/__init__.py b/src/c3nav/mapdata/models/__init__.py index ebbac21f..d9e5d5c2 100644 --- a/src/c3nav/mapdata/models/__init__.py +++ b/src/c3nav/mapdata/models/__init__.py @@ -1,4 +1,4 @@ -from .feature import Feature, FEATURE_TYPES # noqa +from .features import Feature, FEATURE_TYPES # noqa from .level import Level # noqa from .package import Package # noqa from .source import Source # noqa diff --git a/src/c3nav/mapdata/models/base.py b/src/c3nav/mapdata/models/base.py new file mode 100644 index 00000000..53be2bbd --- /dev/null +++ b/src/c3nav/mapdata/models/base.py @@ -0,0 +1,33 @@ +from collections import OrderedDict + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class MapdataModel(models.Model): + name = models.SlugField(_('Name'), max_length=50) + package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, verbose_name=_('map package')) + + @classmethod + def get_path_prefix(cls): + return cls._meta.default_related_name + '/' + + @classmethod + def get_path_regex(cls): + return r'^' + cls.get_path_prefix() + + def get_filename(self): + return self._meta.default_related_name + '/' + self.name + '.json' + + @classmethod + def fromfile(cls, data, file_path): + kwargs = {} + return kwargs + + def tofile(self): + return OrderedDict() + + class Meta: + abstract = True + unique_together = ('package', 'name') + diff --git a/src/c3nav/mapdata/models/feature.py b/src/c3nav/mapdata/models/feature.py deleted file mode 100644 index 5586596d..00000000 --- a/src/c3nav/mapdata/models/feature.py +++ /dev/null @@ -1,105 +0,0 @@ -import os -from collections import OrderedDict, namedtuple - -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.utils.translation import activate, get_language -from shapely.geometry import mapping, shape - -from c3nav.mapdata.fields import GeometryField, JSONField -from c3nav.mapdata.utils import format_geojson - - -class FeatureType(namedtuple('FeatureType', ('name', 'title', 'title_plural', 'geomtype', 'color'))): - # noinspection PyUnusedLocal - def __init__(self, *args, **kwargs): - super().__init__() - FEATURE_TYPES[self.name] = self - - @property - def title_en(self): - language = get_language() - activate('en') - title = str(self.title) - activate(language) - return title - - -FEATURE_TYPES = OrderedDict() -FeatureType('building', _('Building'), _('Buildings'), 'polygon', '#333333') -FeatureType('room', _('Room'), _('Rooms'), 'polygon', '#FFFFFF') -FeatureType('outside', _('Outside Area'), _('Outside Areas'), 'polygon', '#FFFFFF') -FeatureType('obstacle', _('Obstacle'), _('Obstacles'), 'polygon', '#999999') - - -# FeatureType('door', _('Door'), 'polygon', '#FF00FF') -# FeatureType('step', _('Step'), 'polyline', '#FF0000') -# FeatureType('elevator', _('Elevator'), 'polygon', '#99CC00') - - -class Feature(models.Model): - """ - A map feature - """ - TYPES = tuple((name, t.title) for name, t in FEATURE_TYPES.items()) - - name = models.SlugField(_('feature identifier'), unique=True, max_length=50) - package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='features', - verbose_name=_('map package')) - feature_type = models.CharField(max_length=50, choices=TYPES) - level = models.ForeignKey('mapdata.Level', on_delete=models.CASCADE, related_name='features', - verbose_name=_('level')) - titles = JSONField() - geometry = GeometryField() - - path_regex = r'^features/('+'|'.join(name for name, title in TYPES)+')/' - - @property - def title(self): - lang = get_language() - if lang in self.titles: - return self.titles[lang] - return next(iter(self.titles.values())) if self.titles else self.name - - def tofilename(self): - return 'features/%s/%s.json' % (self.feature_type, self.name) - - def get_feature_type(self): - return FEATURE_TYPES[self.feature_type] - - @classmethod - def fromfile(cls, data, file_path): - kwargs = {} - kwargs['feature_type'] = file_path.split(os.path.sep)[1] - - if 'geometry' not in data: - raise ValueError('missing geometry.') - try: - kwargs['geometry'] = shape(data['geometry']) - except: - raise ValueError(_('Invalid GeoJSON.')) - - if 'level' not in data: - raise ValueError('missing level.') - kwargs['level'] = data['level'] - - if 'titles' not in data: - raise ValueError('missing titles.') - titles = data['titles'] - if not isinstance(titles, dict): - raise ValueError('Invalid titles format.') - if any(not isinstance(lang, str) for lang in titles.keys()): - raise ValueError('titles: All languages have to be strings.') - if any(not isinstance(title, str) for title in titles.values()): - raise ValueError('titles: All titles have to be strings.') - if any(not title for title in titles.values()): - raise ValueError('titles: Titles must not be empty strings.') - kwargs['titles'] = titles - return kwargs - - def tofile(self): - return OrderedDict(( - ('titles', OrderedDict(sorted(self.titles.items()))), - ('level', self.level.name), - ('geometry', format_geojson(mapping(self.geometry))) - )) diff --git a/src/c3nav/mapdata/models/features.py b/src/c3nav/mapdata/models/features.py new file mode 100644 index 00000000..7cf563c2 --- /dev/null +++ b/src/c3nav/mapdata/models/features.py @@ -0,0 +1,81 @@ +import os +from collections import OrderedDict + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from shapely.geometry.geo import shape, mapping + +from c3nav.mapdata.fields import GeometryField +from c3nav.mapdata.models.base import MapdataModel +from c3nav.mapdata.utils import format_geojson + + +FEATURE_TYPES = OrderedDict() + +def register_featuretype(cls): + FEATURE_TYPES[cls.__name__.lower()] = cls + return cls + + +class Feature(MapdataModel): + """ + A map feature + """ + level = models.ForeignKey('mapdata.Level', on_delete=models.CASCADE, verbose_name=_('level')) + geometry = GeometryField() + + class Meta: + abstract = True + + @classmethod + def fromfile(cls, data, file_path): + kwargs = super().fromfile(data, file_path) + + if 'geometry' not in data: + raise ValueError('missing geometry.') + try: + kwargs['geometry'] = shape(data['geometry']) + except: + raise ValueError(_('Invalid GeoJSON.')) + + if 'level' not in data: + raise ValueError('missing level.') + kwargs['level'] = data['level'] + + return kwargs + + def tofile(self): + result = super().tofile() + result['level'] = self.level.name + result['geometry'] = format_geojson(mapping(self.geometry)) + return result + + +@register_featuretype +class Inside(Feature): + """ + The outline of a building on a specific level + """ + geomtype = 'polygon' + color = '#333333' + + class Meta: + verbose_name = _('Inside Area') + verbose_name_plural = _('Inside Areas') + default_related_name = 'insides' + + +@register_featuretype +class Room(Feature): + """ + A room inside + """ + geomtype = 'polygon' + color = '#FFFFFF' + + class Meta: + verbose_name = _('Room') + verbose_name_plural = _('Rooms') + default_related_name = 'rooms' + + diff --git a/src/c3nav/mapdata/models/level.py b/src/c3nav/mapdata/models/level.py index 4c817bd1..96fc65e8 100644 --- a/src/c3nav/mapdata/models/level.py +++ b/src/c3nav/mapdata/models/level.py @@ -1,8 +1,10 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from c3nav.mapdata.models.base import MapdataModel -class Level(models.Model): + +class Level(MapdataModel): """ A map level (-1, 0, 1, 2…) """ @@ -12,24 +14,29 @@ class Level(models.Model): package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='levels', verbose_name=_('map package')) - path_regex = r'^levels/' + class Meta: + verbose_name = _('Level') + verbose_name_plural = _('Levels') + default_related_name = 'levels' def tofilename(self): return 'levels/%s.json' % self.name @classmethod def fromfile(cls, data, file_path): + kwargs = super().fromfile(data, file_path) + if 'altitude' not in data: raise ValueError('missing altitude.') if not isinstance(data['altitude'], (int, float)): raise ValueError('altitude has to be int or float.') - return { - 'altitude': data['altitude'], - } + kwargs['altitude'] = data['altitude'] + + return kwargs def tofile(self): - return { - 'altitude': float(self.altitude) - } + result = super().tofile() + result['altitude'] = float(self.altitude) + return result diff --git a/src/c3nav/mapdata/models/package.py b/src/c3nav/mapdata/models/package.py index b9aee494..89ef0b57 100644 --- a/src/c3nav/mapdata/models/package.py +++ b/src/c3nav/mapdata/models/package.py @@ -22,7 +22,17 @@ class Package(models.Model): directory = models.CharField(_('folder name'), max_length=100) - path_regex = r'^package.json$' + class Meta: + verbose_name = _('Map Package') + verbose_name_plural = _('Map Packages') + default_related_name = 'packages' + + @classmethod + def get_path_regex(cls): + return '^package.json$' + + def get_filename(self): + return 'package.json' @property def package(self): diff --git a/src/c3nav/mapdata/models/source.py b/src/c3nav/mapdata/models/source.py index ded8e93e..69dd7087 100644 --- a/src/c3nav/mapdata/models/source.py +++ b/src/c3nav/mapdata/models/source.py @@ -1,21 +1,22 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from c3nav.mapdata.models.base import MapdataModel -class Source(models.Model): + +class Source(MapdataModel): """ A map source, images of levels that can be useful as backgrounds for the map editor """ - name = models.SlugField(_('source name'), unique=True, max_length=50) - package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='sources', - verbose_name=_('map package')) - bottom = models.DecimalField(_('bottom coordinate'), max_digits=6, decimal_places=2) left = models.DecimalField(_('left coordinate'), max_digits=6, decimal_places=2) top = models.DecimalField(_('top coordinate'), max_digits=6, decimal_places=2) right = models.DecimalField(_('right coordinate'), max_digits=6, decimal_places=2) - path_regex = r'^sources/' + class Meta: + verbose_name = _('Source') + verbose_name_plural = _('Sources') + default_related_name = 'sources' @classmethod def max_bounds(cls): @@ -28,12 +29,9 @@ class Source(models.Model): def bounds(self): return (float(self.bottom), float(self.left)), (float(self.top), float(self.right)) - def tofilename(self): - return 'sources/%s.json' % self.name - @classmethod def fromfile(cls, data, file_path): - kwargs = {} + kwargs = super().fromfile(data, file_path) if 'bounds' not in data: raise ValueError('missing bounds.') @@ -50,6 +48,6 @@ class Source(models.Model): return kwargs def tofile(self): - return { - 'bounds': ((float(self.bottom), float(self.left)), (float(self.top), float(self.right))) - } + result = super().tofile() + result['bounds'] = ((float(self.bottom), float(self.left)), (float(self.top), float(self.right))) + return result diff --git a/src/c3nav/mapdata/packageio/const.py b/src/c3nav/mapdata/packageio/const.py index d1192f35..a2dd7456 100644 --- a/src/c3nav/mapdata/packageio/const.py +++ b/src/c3nav/mapdata/packageio/const.py @@ -1,3 +1,4 @@ -from c3nav.mapdata.models import Feature, Level, Package, Source +from c3nav.mapdata.models import Level, Package, Source +from c3nav.mapdata.models.features import Inside, Room -ordered_models = (Package, Level, Source, Feature) +ordered_models = (Package, Level, Source, Inside, Room) diff --git a/src/c3nav/mapdata/packageio/read.py b/src/c3nav/mapdata/packageio/read.py index 79b2944d..7db8bfd6 100644 --- a/src/c3nav/mapdata/packageio/read.py +++ b/src/c3nav/mapdata/packageio/read.py @@ -2,6 +2,7 @@ import json import os import re import subprocess +from collections import OrderedDict from django.conf import settings from django.core.management import CommandError @@ -15,6 +16,8 @@ class MapdataReader: self.content = {} self.package_names_by_dir = {} self.saved_items = {model: {} for model in ordered_models} + self.path_regexes = OrderedDict((model, model.get_path_regex()) for model in ordered_models) + print(self.path_regexes) def read_packages(self): print('Detecting Map Packages…') @@ -44,8 +47,8 @@ class MapdataReader: file_path = os.path.join(package_dir, path, filename) relative_file_path = os.path.join(path, filename) print(file_path) - for model in ordered_models: - if re.search(model.path_regex, relative_file_path): + for model, path_regex in self.path_regexes.items(): + if re.search(path_regex, relative_file_path): self._add_item(ReaderItem(self, package_dir, path, filename, model)) break else: diff --git a/src/c3nav/mapdata/packageio/write.py b/src/c3nav/mapdata/packageio/write.py index 0b4350ec..424941ee 100644 --- a/src/c3nav/mapdata/packageio/write.py +++ b/src/c3nav/mapdata/packageio/write.py @@ -24,7 +24,7 @@ class MapdataWriter: count = 0 for model in ordered_models: for obj in model.objects.all().order_by('name').prefetch_related(): - file_path = os.path.join(obj.package.directory, obj.tofilename()) + file_path = os.path.join(obj.package.directory, obj.get_filename()) full_file_path = os.path.join(settings.MAP_ROOT, file_path) self.keep.add(file_path) diff --git a/src/c3nav/mapdata/serializers.py b/src/c3nav/mapdata/serializers.py index 42492e08..291a693f 100644 --- a/src/c3nav/mapdata/serializers.py +++ b/src/c3nav/mapdata/serializers.py @@ -60,18 +60,35 @@ class SourceSerializer(serializers.ModelSerializer): class FeatureTypeSerializer(serializers.Serializer): - name = serializers.CharField() - title = serializers.CharField() - title_plural = serializers.CharField() + name = serializers.SerializerMethodField() + title = serializers.SerializerMethodField() + title_plural = serializers.SerializerMethodField() geomtype = serializers.CharField() color = serializers.CharField() + def get_name(self, obj): + return obj.__name__.lower() -class FeatureSerializer(serializers.ModelSerializer): - level = serializers.SlugRelatedField(slug_field='name', read_only=True) - titles = serializers.JSONField() + def get_title(self, obj): + return str(obj._meta.verbose_name) + + def get_title_plural(self, obj): + return str(obj._meta.verbose_name_plural) + + +class FeatureSerializer(serializers.Serializer): + name = serializers.CharField() + feature_type = serializers.SerializerMethodField() + level = serializers.SerializerMethodField() + package = serializers.SerializerMethodField() geometry = GeometryField() - class Meta: - model = Feature - fields = ('name', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry') + def get_feature_type(self, obj): + return obj.__class__.__name__.lower() + + def get_level(self, obj): + return obj.level.name + + def get_package(self, obj): + return obj.package.name +