add LevelConnector
This commit is contained in:
parent
8164cc1a40
commit
b309b6f6cd
10 changed files with 164 additions and 28 deletions
|
@ -6,6 +6,7 @@ from django.conf import settings
|
||||||
from django.forms import CharField, ModelForm, ValidationError
|
from django.forms import CharField, ModelForm, ValidationError
|
||||||
from django.forms.models import ModelChoiceField
|
from django.forms.models import ModelChoiceField
|
||||||
from django.forms.widgets import HiddenInput
|
from django.forms.widgets import HiddenInput
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from shapely.geometry.geo import mapping
|
from shapely.geometry.geo import mapping
|
||||||
|
|
||||||
from c3nav.mapdata.models import Package
|
from c3nav.mapdata.models import Package
|
||||||
|
@ -50,6 +51,10 @@ class MapitemFormMixin(ModelForm):
|
||||||
if not creating:
|
if not creating:
|
||||||
self.initial['level'] = self.instance.level.name
|
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:
|
if 'geometry' in self.fields:
|
||||||
# hide geometry widget
|
# hide geometry widget
|
||||||
self.fields['geometry'].widget = HiddenInput()
|
self.fields['geometry'].widget = HiddenInput()
|
||||||
|
@ -73,6 +78,12 @@ class MapitemFormMixin(ModelForm):
|
||||||
initial=titles[language].strip(), max_length=50)
|
initial=titles[language].strip(), max_length=50)
|
||||||
self.titles = titles
|
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):
|
def clean(self):
|
||||||
if 'geometry' in self.fields:
|
if 'geometry' in self.fields:
|
||||||
if not self.cleaned_data.get('geometry'):
|
if not self.cleaned_data.get('geometry'):
|
||||||
|
@ -80,7 +91,7 @@ class MapitemFormMixin(ModelForm):
|
||||||
|
|
||||||
|
|
||||||
def create_editor_form(mapitemtype):
|
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)]
|
existing_fields = [field for field in possible_fields if hasattr(mapitemtype, field)]
|
||||||
|
|
||||||
class EditorForm(MapitemFormMixin, ModelForm):
|
class EditorForm(MapitemFormMixin, ModelForm):
|
||||||
|
|
|
@ -160,6 +160,7 @@ editor = {
|
||||||
'door': '#FF00FF',
|
'door': '#FF00FF',
|
||||||
'hole': '#66CC66',
|
'hole': '#66CC66',
|
||||||
'elevatorlevel': '#9EF8FB',
|
'elevatorlevel': '#9EF8FB',
|
||||||
|
'levelconnector': '#FFFF00'
|
||||||
},
|
},
|
||||||
_get_geometry_style: function (feature) {
|
_get_geometry_style: function (feature) {
|
||||||
// style callback for GeoJSON loader
|
// style callback for GeoJSON loader
|
||||||
|
@ -281,6 +282,7 @@ editor = {
|
||||||
}
|
}
|
||||||
editor._creating = true;
|
editor._creating = true;
|
||||||
$('#id_level').val(editor._level);
|
$('#id_level').val(editor._level);
|
||||||
|
$('#id_levels').find('option[value='+editor._level+']').prop('selected', true);
|
||||||
}
|
}
|
||||||
} else if (editor._get_geometries_next_time) {
|
} else if (editor._get_geometries_next_time) {
|
||||||
editor.get_geometries();
|
editor.get_geometries();
|
||||||
|
|
|
@ -15,9 +15,13 @@ from c3nav.mapdata.permissions import can_access_package, filter_queryset_by_pac
|
||||||
|
|
||||||
def list_mapitemtypes(request, level):
|
def list_mapitemtypes(request, level):
|
||||||
def get_item_count(mapitemtype):
|
def get_item_count(mapitemtype):
|
||||||
if not hasattr(mapitemtype, 'level'):
|
if hasattr(mapitemtype, 'level'):
|
||||||
return 0
|
return filter_queryset_by_package_access(request, mapitemtype.objects.filter(level__name=level)).count()
|
||||||
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', {
|
return render(request, 'editor/mapitemtypes.html', {
|
||||||
'level': level,
|
'level': level,
|
||||||
|
@ -25,7 +29,7 @@ def list_mapitemtypes(request, level):
|
||||||
{
|
{
|
||||||
'name': name,
|
'name': name,
|
||||||
'title': mapitemtype._meta.verbose_name_plural,
|
'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),
|
'count': get_item_count(mapitemtype),
|
||||||
} for name, mapitemtype in MAPITEM_TYPES.items()
|
} for name, mapitemtype in MAPITEM_TYPES.items()
|
||||||
],
|
],
|
||||||
|
@ -37,14 +41,18 @@ def list_mapitems(request, mapitem_type, level=None):
|
||||||
if mapitemtype is None:
|
if mapitemtype is None:
|
||||||
raise Http404('Unknown mapitemtype.')
|
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.')
|
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)
|
return redirect('editor.mapitems', mapitem_type=mapitem_type)
|
||||||
|
|
||||||
queryset = mapitemtype.objects.all()
|
queryset = mapitemtype.objects.all()
|
||||||
if level is not None:
|
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', {
|
return render(request, 'editor/mapitems.html', {
|
||||||
'mapitem_type': mapitem_type,
|
'mapitem_type': mapitem_type,
|
||||||
|
@ -105,7 +113,7 @@ def edit_mapitem(request, mapitem_type, name=None):
|
||||||
# Update/create mapitem
|
# Update/create mapitem
|
||||||
commit_type = 'Created' if mapitem is None else 'Updated'
|
commit_type = 'Created' if mapitem is None else 'Updated'
|
||||||
action = 'create' if mapitem is None else 'edit'
|
action = 'create' if mapitem is None else 'edit'
|
||||||
mapitem = form.instance
|
mapitem = form.save(commit=False)
|
||||||
|
|
||||||
if form.titles is not None:
|
if form.titles is not None:
|
||||||
mapitem.titles = {}
|
mapitem.titles = {}
|
||||||
|
@ -130,6 +138,7 @@ def edit_mapitem(request, mapitem_type, name=None):
|
||||||
})
|
})
|
||||||
|
|
||||||
mapitem.save()
|
mapitem.save()
|
||||||
|
form.save_m2m()
|
||||||
|
|
||||||
return render(request, 'editor/mapitem_success.html', {
|
return render(request, 'editor/mapitem_success.html', {
|
||||||
'mapitem_type': mapitem_type
|
'mapitem_type': mapitem_type
|
||||||
|
|
|
@ -38,7 +38,12 @@ class GeometryViewSet(ViewSet):
|
||||||
if packages:
|
if packages:
|
||||||
queryset = queryset.filter(package__name__in=packages)
|
queryset = queryset.filter(package__name__in=packages)
|
||||||
if levels:
|
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:
|
if names:
|
||||||
queryset = queryset.filter(name__in=names)
|
queryset = queryset.filter(name__in=names)
|
||||||
queryset = filter_queryset_by_package_access(request, queryset)
|
queryset = filter_queryset_by_package_access(request, queryset)
|
||||||
|
|
32
src/c3nav/mapdata/migrations/0009_levelconnector.py
Normal file
32
src/c3nav/mapdata/migrations/0009_levelconnector.py
Normal file
|
@ -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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,4 +2,4 @@ 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
|
||||||
from .collections import Elevator # noqa
|
from .collections import Elevator # noqa
|
||||||
from .geometry import GeometryMapItem, GEOMETRY_MAPITEM_TYPES # noqa
|
from .geometry import GeometryMapItemWithLevel, GEOMETRY_MAPITEM_TYPES # noqa
|
||||||
|
|
|
@ -24,7 +24,6 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta):
|
||||||
"""
|
"""
|
||||||
A map feature
|
A map feature
|
||||||
"""
|
"""
|
||||||
level = models.ForeignKey('mapdata.Level', on_delete=models.CASCADE, verbose_name=_('level'))
|
|
||||||
geometry = GeometryField()
|
geometry = GeometryField()
|
||||||
|
|
||||||
geomtype = None
|
geomtype = None
|
||||||
|
@ -47,10 +46,6 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta):
|
||||||
except:
|
except:
|
||||||
raise ValueError(_('Invalid GeoJSON.'))
|
raise ValueError(_('Invalid GeoJSON.'))
|
||||||
|
|
||||||
if 'level' not in data:
|
|
||||||
raise ValueError('missing level.')
|
|
||||||
kwargs['level'] = data['level']
|
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -64,7 +59,6 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta):
|
||||||
('type', self.__class__.__name__.lower()),
|
('type', self.__class__.__name__.lower()),
|
||||||
('name', self.name),
|
('name', self.name),
|
||||||
('package', self.package.name),
|
('package', self.package.name),
|
||||||
('level', self.level.name),
|
|
||||||
))
|
))
|
||||||
|
|
||||||
def to_geojson(self):
|
def to_geojson(self):
|
||||||
|
@ -76,12 +70,81 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta):
|
||||||
|
|
||||||
def tofile(self):
|
def tofile(self):
|
||||||
result = super().tofile()
|
result = super().tofile()
|
||||||
result['level'] = self.level.name
|
|
||||||
result['geometry'] = format_geojson(mapping(self.geometry))
|
result['geometry'] = format_geojson(mapping(self.geometry))
|
||||||
return result
|
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
|
The outline of a building on a specific level
|
||||||
"""
|
"""
|
||||||
|
@ -93,7 +156,7 @@ class Building(GeometryMapItem):
|
||||||
default_related_name = 'buildings'
|
default_related_name = 'buildings'
|
||||||
|
|
||||||
|
|
||||||
class Room(GeometryMapItem):
|
class Room(GeometryMapItemWithLevel):
|
||||||
"""
|
"""
|
||||||
An accessible area like a room. Can overlap.
|
An accessible area like a room. Can overlap.
|
||||||
"""
|
"""
|
||||||
|
@ -105,7 +168,7 @@ class Room(GeometryMapItem):
|
||||||
default_related_name = 'rooms'
|
default_related_name = 'rooms'
|
||||||
|
|
||||||
|
|
||||||
class Outside(GeometryMapItem):
|
class Outside(GeometryMapItemWithLevel):
|
||||||
"""
|
"""
|
||||||
An accessible outdoor area like a court. Can overlap.
|
An accessible outdoor area like a court. Can overlap.
|
||||||
"""
|
"""
|
||||||
|
@ -117,7 +180,7 @@ class Outside(GeometryMapItem):
|
||||||
default_related_name = 'outsides'
|
default_related_name = 'outsides'
|
||||||
|
|
||||||
|
|
||||||
class Obstacle(GeometryMapItem):
|
class Obstacle(GeometryMapItemWithLevel):
|
||||||
"""
|
"""
|
||||||
An obstacle
|
An obstacle
|
||||||
"""
|
"""
|
||||||
|
@ -153,7 +216,7 @@ class Obstacle(GeometryMapItem):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class Door(GeometryMapItem):
|
class Door(GeometryMapItemWithLevel):
|
||||||
"""
|
"""
|
||||||
A connection between two rooms
|
A connection between two rooms
|
||||||
"""
|
"""
|
||||||
|
@ -165,7 +228,7 @@ class Door(GeometryMapItem):
|
||||||
default_related_name = 'doors'
|
default_related_name = 'doors'
|
||||||
|
|
||||||
|
|
||||||
class Hole(GeometryMapItem):
|
class Hole(GeometryMapItemWithLevel):
|
||||||
"""
|
"""
|
||||||
A hole in the ground of a room, e.g. for stairs.
|
A hole in the ground of a room, e.g. for stairs.
|
||||||
"""
|
"""
|
||||||
|
@ -177,11 +240,11 @@ class Hole(GeometryMapItem):
|
||||||
default_related_name = 'holes'
|
default_related_name = 'holes'
|
||||||
|
|
||||||
|
|
||||||
class ElevatorLevel(GeometryMapItem):
|
class ElevatorLevel(GeometryMapItemWithLevel):
|
||||||
"""
|
"""
|
||||||
An elevator Level
|
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)
|
button = models.SlugField(_('Button label'), max_length=10)
|
||||||
|
|
||||||
geomtype = 'polygon'
|
geomtype = 'polygon'
|
||||||
|
|
|
@ -46,6 +46,9 @@ class Level(MapItem):
|
||||||
result['altitude'] = float(self.altitude)
|
result['altitude'] = float(self.altitude)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class LevelGeometries():
|
class LevelGeometries():
|
||||||
def __init__(self, level):
|
def __init__(self, level):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from c3nav.mapdata.models import Level, Package, Source
|
from c3nav.mapdata.models import Level, Package, Source
|
||||||
from c3nav.mapdata.models.collections import Elevator
|
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)
|
ordered_models += (Elevator, ElevatorLevel)
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.conf import settings
|
||||||
from django.core.management import CommandError
|
from django.core.management import CommandError
|
||||||
|
|
||||||
from c3nav.mapdata.models import Elevator, Level, Package
|
from c3nav.mapdata.models import Elevator, Level, Package
|
||||||
|
from c3nav.mapdata.models.geometry import LevelConnector
|
||||||
from c3nav.mapdata.packageio.const import ordered_models
|
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']]
|
depends = [self.reader.saved_items[Package][name].obj.pk for name in self.data['depends']]
|
||||||
self.data.pop('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
|
# Change name references to the referenced object
|
||||||
for name, model in self.relations.items():
|
for name, model in self.relations.items():
|
||||||
if name in self.data:
|
if name in self.data:
|
||||||
|
@ -174,3 +180,8 @@ class ReaderItem:
|
||||||
self.obj.depends.clear()
|
self.obj.depends.clear()
|
||||||
for dependency in depends:
|
for dependency in depends:
|
||||||
self.obj.depends.add(dependency)
|
self.obj.depends.add(dependency)
|
||||||
|
|
||||||
|
if levels:
|
||||||
|
self.obj.levels.clear()
|
||||||
|
for level in levels:
|
||||||
|
self.obj.levels.add(level)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue