diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 135eb1d0..bd401582 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -99,7 +99,7 @@ class MapitemFormMixin(ModelForm): def create_editor_form(mapitemtype): possible_fields = ['name', 'package', 'altitude', 'level', 'intermediate', 'levels', 'geometry', - 'elevator', 'button', 'crop_to_level'] + 'elevator', 'button', 'crop_to_level', 'width'] 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 16884b15..b6150b80 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -261,6 +261,7 @@ editor = { 'building': '#333333', 'room': '#FFFFFF', 'outside': '#EEFFEE', + 'lineobstacle': '#999999', 'obstacle': '#999999', 'door': '#FF00FF', 'hole': '#66CC66', diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 3eb5cb68..74c9185e 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -10,7 +10,7 @@ from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet from c3nav.mapdata.models import GEOMETRY_MAPITEM_TYPES, Level, Package, Source -from c3nav.mapdata.models.geometry import LineGeometryMapItemWithLevel +from c3nav.mapdata.models.geometry import DirectedLineGeometryMapItemWithLevel from c3nav.mapdata.permissions import filter_queryset_by_package_access, get_unlocked_packages_names from c3nav.mapdata.serializers.main import LevelSerializer, PackageSerializer, SourceSerializer from c3nav.mapdata.utils.cache import (CachedReadOnlyViewSetMixin, cache_mapdata_api_response, get_levels_cached, @@ -93,7 +93,7 @@ class GeometryViewSet(ViewSet): if hasattr(mapitemtype, field_name): queryset.prefetch_related(field_name) - if issubclass(mapitemtype, LineGeometryMapItemWithLevel): + if issubclass(mapitemtype, DirectedLineGeometryMapItemWithLevel): results.extend(obj.to_shadow_geojson() for obj in queryset) results.extend(obj.to_geojson() for obj in queryset) diff --git a/src/c3nav/mapdata/migrations/0014_lineobstacle.py b/src/c3nav/mapdata/migrations/0014_lineobstacle.py new file mode 100644 index 00000000..ebaba462 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0014_lineobstacle.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-08 20:20 +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', '0013_auto_20161208_0937'), + ] + + operations = [ + migrations.CreateModel( + name='LineObstacle', + 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()), + ('altitude', models.DecimalField(decimal_places=2, max_digits=4, verbose_name='obstacle width')), + ('level', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lineobstacles', to='mapdata.Level', verbose_name='level')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lineobstacles', to='mapdata.Package', verbose_name='map package')), + ], + options={ + 'verbose_name_plural': 'Line Obstacles', + 'default_related_name': 'lineobstacles', + 'verbose_name': 'Line Obstacle', + }, + ), + ] diff --git a/src/c3nav/mapdata/migrations/0015_auto_20161208_2020.py b/src/c3nav/mapdata/migrations/0015_auto_20161208_2020.py new file mode 100644 index 00000000..79921299 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0015_auto_20161208_2020.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-08 20:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0014_lineobstacle'), + ] + + operations = [ + migrations.AlterField( + model_name='lineobstacle', + name='altitude', + field=models.DecimalField(decimal_places=2, default=0.2, max_digits=4, verbose_name='obstacle width'), + ), + ] diff --git a/src/c3nav/mapdata/migrations/0016_auto_20161208_2023.py b/src/c3nav/mapdata/migrations/0016_auto_20161208_2023.py new file mode 100644 index 00000000..5cb1557d --- /dev/null +++ b/src/c3nav/mapdata/migrations/0016_auto_20161208_2023.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-08 20:23 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0015_auto_20161208_2020'), + ] + + operations = [ + migrations.RenameField( + model_name='lineobstacle', + old_name='altitude', + new_name='width', + ), + ] diff --git a/src/c3nav/mapdata/migrations/0017_auto_20161208_2039.py b/src/c3nav/mapdata/migrations/0017_auto_20161208_2039.py new file mode 100644 index 00000000..251776f2 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0017_auto_20161208_2039.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-08 20:39 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0016_auto_20161208_2023'), + ] + + operations = [ + migrations.AlterField( + model_name='lineobstacle', + name='width', + field=models.DecimalField(decimal_places=2, default=0.15, max_digits=4, verbose_name='obstacle width'), + ), + ] diff --git a/src/c3nav/mapdata/models/geometry.py b/src/c3nav/mapdata/models/geometry.py index 98810e77..671175e3 100644 --- a/src/c3nav/mapdata/models/geometry.py +++ b/src/c3nav/mapdata/models/geometry.py @@ -103,7 +103,7 @@ class GeometryMapItemWithLevel(GeometryMapItem): return result -class LineGeometryMapItemWithLevel(GeometryMapItemWithLevel): +class DirectedLineGeometryMapItemWithLevel(GeometryMapItemWithLevel): geomtype = 'polyline' class Meta: @@ -168,7 +168,7 @@ class Outside(GeometryMapItemWithLevel): default_related_name = 'outsides' -class Stair(LineGeometryMapItemWithLevel): +class Stair(DirectedLineGeometryMapItemWithLevel): """ A stair """ @@ -178,6 +178,48 @@ class Stair(LineGeometryMapItemWithLevel): default_related_name = 'stairs' +class LineObstacle(GeometryMapItemWithLevel): + """ + An obstacle that is a line with a specific width + """ + width = models.DecimalField(_('obstacle width'), max_digits=4, decimal_places=2, default=0.15) + + geomtype = 'polyline' + + class Meta: + verbose_name = _('Line Obstacle') + verbose_name_plural = _('Line Obstacles') + default_related_name = 'lineobstacles' + + def to_geojson(self): + result = super().to_geojson() + original_geometry = result['geometry'] + draw = self.geometry.buffer(self.width/2, join_style=JOIN_STYLE.mitre, cap_style=CAP_STYLE.flat) + result['geometry'] = format_geojson(mapping(draw)) + result['original_geometry'] = original_geometry + return result + + @classmethod + def fromfile(cls, data, file_path): + kwargs = super().fromfile(data, file_path) + + if 'width' not in data: + raise ValueError('missing width.') + kwargs['width'] = data['width'] + + return kwargs + + def get_geojson_properties(self): + result = super().get_geojson_properties() + result['width'] = float(self.width) + return result + + def tofile(self): + result = super().tofile() + result['width'] = float(self.width) + return result + + class Obstacle(GeometryMapItemWithLevel): """ An obstacle diff --git a/src/c3nav/mapdata/models/level.py b/src/c3nav/mapdata/models/level.py index 4ea472aa..55e4818f 100644 --- a/src/c3nav/mapdata/models/level.py +++ b/src/c3nav/mapdata/models/level.py @@ -104,6 +104,14 @@ class LevelGeometries(): def mapped(self): return cascaded_union([self.buildings, self.outsides]) + @cached_property + def lineobstacles(self): + lineobstacles = [] + for obstacle in self.level.lineobstacles.all(): + lineobstacles.append(obstacle.geometry.buffer(obstacle.width/2, + join_style=JOIN_STYLE.mitre, cap_style=CAP_STYLE.flat)) + return cascaded_union(lineobstacles) + @cached_property def obstacles(self): levels_by_name = {} @@ -119,6 +127,7 @@ class LevelGeometries(): if level_name is not None: obstacles = obstacles.intersection(levels_by_name[level_name].geometries.mapped) all_obstacles.append(obstacles) + all_obstacles.extend(self.lineobstacles) return cascaded_union(all_obstacles).intersection(self.mapped)