split feature model – beginnings (still missing: API, Forms, Editor)

This commit is contained in:
Laura Klünder 2016-10-12 15:25:00 +02:00
parent 55a7e20df1
commit 0036b27057
15 changed files with 238 additions and 156 deletions

View file

@ -15,7 +15,7 @@ router.register(r'levels', LevelViewSet)
router.register(r'packages', PackageViewSet) router.register(r'packages', PackageViewSet)
router.register(r'sources', SourceViewSet) router.register(r'sources', SourceViewSet)
router.register(r'featuretypes', FeatureTypeViewSet, base_name='featuretype') 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'hosters', HosterViewSet, base_name='hoster')
router.register(r'submittasks', SubmitTaskViewSet, base_name='submittask') router.register(r'submittasks', SubmitTaskViewSet, base_name='submittask')

View file

@ -5,9 +5,9 @@ from django.core.signing import BadSignature
from django.http.response import Http404 from django.http.response import Http404
from django.shortcuts import get_object_or_404, render 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.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.models.package import Package
from c3nav.mapdata.packageio.write import json_encode from c3nav.mapdata.packageio.write import json_encode
from c3nav.mapdata.permissions import can_access_package from c3nav.mapdata.permissions import can_access_package

View file

@ -1,5 +1,6 @@
import mimetypes import mimetypes
import os import os
from itertools import chain
from django.conf import settings from django.conf import settings
from django.core.files import File 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.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet 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.permissions import filter_source_queryset
from c3nav.mapdata.serializers import (FeatureSerializer, FeatureTypeSerializer, LevelSerializer, PackageSerializer, from c3nav.mapdata.serializers import (FeatureSerializer, FeatureTypeSerializer, LevelSerializer, PackageSerializer,
SourceSerializer) SourceSerializer)
@ -89,7 +91,15 @@ class FeatureViewSet(ReadOnlyModelViewSet):
""" """
List and retrieve map features you have access to List and retrieve map features you have access to
""" """
queryset = Feature.objects.all() model = Feature
base_name = 'feature'
serializer_class = FeatureSerializer serializer_class = FeatureSerializer
lookup_field = 'name' lookup_field = 'name'
lookup_value_regex = '[^/]+' lookup_value_regex = '[^/]+'
def get_queryset(self):
querysets = []
for name, model in FEATURE_TYPES.items():
querysets.append(model.objects.all())
return chain(*querysets)

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- 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 from __future__ import unicode_literals
import c3nav.mapdata.fields import c3nav.mapdata.fields
@ -16,14 +16,17 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Feature', name='Inside',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.SlugField(unique=True, verbose_name='feature identifier')), ('name', models.SlugField(unique=True, verbose_name='name')),
('feature_type', models.CharField(choices=[('building', 'Building'), ('room', 'Room'), ('outside', 'Outside Area'), ('obstacle', 'Obstacle')], max_length=50)),
('titles', c3nav.mapdata.fields.JSONField()),
('geometry', c3nav.mapdata.fields.GeometryField()), ('geometry', c3nav.mapdata.fields.GeometryField()),
], ],
options={
'default_related_name': 'insides',
'verbose_name': 'Inside Area',
'verbose_name_plural': 'Inside Areas',
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Level', 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')), ('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')), ('altitude', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='level altitude')),
], ],
options={
'abstract': False,
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Package', name='Package',
@ -47,6 +53,24 @@ class Migration(migrations.Migration):
('directory', models.CharField(max_length=100, verbose_name='folder name')), ('directory', models.CharField(max_length=100, verbose_name='folder name')),
('depends', models.ManyToManyField(to='mapdata.Package')), ('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( migrations.CreateModel(
name='Source', name='Source',
@ -59,6 +83,9 @@ class Migration(migrations.Migration):
('right', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='right coordinate')), ('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')), ('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( migrations.AddField(
model_name='level', 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'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='mapdata.Package', verbose_name='map package'),
), ),
migrations.AddField( migrations.AddField(
model_name='feature', model_name='inside',
name='level', 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( migrations.AddField(
model_name='feature', model_name='inside',
name='package', 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'),
), ),
] ]

View file

@ -1,4 +1,4 @@
from .feature import Feature, FEATURE_TYPES # noqa from .features import Feature, FEATURE_TYPES # noqa
from .level import Level # noqa from .level import Level # noqa
from .package import Package # noqa from .package import Package # noqa
from .source import Source # noqa from .source import Source # noqa

View file

@ -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')

View file

@ -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)))
))

View file

@ -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'

View file

@ -1,8 +1,10 @@
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ 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) 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', package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='levels',
verbose_name=_('map package')) verbose_name=_('map package'))
path_regex = r'^levels/' class Meta:
verbose_name = _('Level')
verbose_name_plural = _('Levels')
default_related_name = 'levels'
def tofilename(self): def tofilename(self):
return 'levels/%s.json' % self.name return 'levels/%s.json' % self.name
@classmethod @classmethod
def fromfile(cls, data, file_path): def fromfile(cls, data, file_path):
kwargs = super().fromfile(data, file_path)
if 'altitude' not in data: if 'altitude' not in data:
raise ValueError('missing altitude.') raise ValueError('missing altitude.')
if not isinstance(data['altitude'], (int, float)): if not isinstance(data['altitude'], (int, float)):
raise ValueError('altitude has to be int or float.') raise ValueError('altitude has to be int or float.')
return { kwargs['altitude'] = data['altitude']
'altitude': data['altitude'],
} return kwargs
def tofile(self): def tofile(self):
return { result = super().tofile()
'altitude': float(self.altitude) result['altitude'] = float(self.altitude)
} return result

View file

@ -22,7 +22,17 @@ class Package(models.Model):
directory = models.CharField(_('folder name'), max_length=100) 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 @property
def package(self): def package(self):

View file

@ -1,21 +1,22 @@
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ 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 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) bottom = models.DecimalField(_('bottom coordinate'), max_digits=6, decimal_places=2)
left = models.DecimalField(_('left 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) top = models.DecimalField(_('top coordinate'), max_digits=6, decimal_places=2)
right = models.DecimalField(_('right 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 @classmethod
def max_bounds(cls): def max_bounds(cls):
@ -28,12 +29,9 @@ class Source(models.Model):
def bounds(self): def bounds(self):
return (float(self.bottom), float(self.left)), (float(self.top), float(self.right)) return (float(self.bottom), float(self.left)), (float(self.top), float(self.right))
def tofilename(self):
return 'sources/%s.json' % self.name
@classmethod @classmethod
def fromfile(cls, data, file_path): def fromfile(cls, data, file_path):
kwargs = {} kwargs = super().fromfile(data, file_path)
if 'bounds' not in data: if 'bounds' not in data:
raise ValueError('missing bounds.') raise ValueError('missing bounds.')
@ -50,6 +48,6 @@ class Source(models.Model):
return kwargs return kwargs
def tofile(self): def tofile(self):
return { result = super().tofile()
'bounds': ((float(self.bottom), float(self.left)), (float(self.top), float(self.right))) result['bounds'] = ((float(self.bottom), float(self.left)), (float(self.top), float(self.right)))
} return result

View file

@ -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)

View file

@ -2,6 +2,7 @@ import json
import os import os
import re import re
import subprocess import subprocess
from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.core.management import CommandError from django.core.management import CommandError
@ -15,6 +16,8 @@ class MapdataReader:
self.content = {} self.content = {}
self.package_names_by_dir = {} self.package_names_by_dir = {}
self.saved_items = {model: {} for model in ordered_models} 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): def read_packages(self):
print('Detecting Map Packages…') print('Detecting Map Packages…')
@ -44,8 +47,8 @@ class MapdataReader:
file_path = os.path.join(package_dir, path, filename) file_path = os.path.join(package_dir, path, filename)
relative_file_path = os.path.join(path, filename) relative_file_path = os.path.join(path, filename)
print(file_path) print(file_path)
for model in ordered_models: for model, path_regex in self.path_regexes.items():
if re.search(model.path_regex, relative_file_path): if re.search(path_regex, relative_file_path):
self._add_item(ReaderItem(self, package_dir, path, filename, model)) self._add_item(ReaderItem(self, package_dir, path, filename, model))
break break
else: else:

View file

@ -24,7 +24,7 @@ class MapdataWriter:
count = 0 count = 0
for model in ordered_models: for model in ordered_models:
for obj in model.objects.all().order_by('name').prefetch_related(): 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) full_file_path = os.path.join(settings.MAP_ROOT, file_path)
self.keep.add(file_path) self.keep.add(file_path)

View file

@ -60,18 +60,35 @@ class SourceSerializer(serializers.ModelSerializer):
class FeatureTypeSerializer(serializers.Serializer): class FeatureTypeSerializer(serializers.Serializer):
name = serializers.CharField() name = serializers.SerializerMethodField()
title = serializers.CharField() title = serializers.SerializerMethodField()
title_plural = serializers.CharField() title_plural = serializers.SerializerMethodField()
geomtype = serializers.CharField() geomtype = serializers.CharField()
color = serializers.CharField() color = serializers.CharField()
def get_name(self, obj):
return obj.__name__.lower()
class FeatureSerializer(serializers.ModelSerializer): def get_title(self, obj):
level = serializers.SlugRelatedField(slug_field='name', read_only=True) return str(obj._meta.verbose_name)
titles = serializers.JSONField()
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() geometry = GeometryField()
class Meta: def get_feature_type(self, obj):
model = Feature return obj.__class__.__name__.lower()
fields = ('name', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry')
def get_level(self, obj):
return obj.level.name
def get_package(self, obj):
return obj.package.name