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

View file

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

View file

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

View file

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

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 .package import Package # 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.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

View file

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

View file

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

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 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:

View file

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

View file

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