From 56b083f714eabca23f5c8911a23df90e4d91f120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 8 Dec 2016 12:36:09 +0100 Subject: [PATCH] add stairs --- src/c3nav/editor/static/editor/js/editor.js | 32 ++++++++-- src/c3nav/mapdata/api.py | 6 +- .../migrations/0013_auto_20161208_0937.py | 44 +++++++++++++ src/c3nav/mapdata/models/geometry.py | 63 +++++++++++++------ src/c3nav/mapdata/models/level.py | 12 ++++ src/c3nav/mapdata/render/renderer.py | 23 +++++-- src/c3nav/mapdata/utils/geometry.py | 19 ++++-- 7 files changed, 167 insertions(+), 32 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0013_auto_20161208_0937.py diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index b7650db1..16884b15 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -168,6 +168,7 @@ editor = { _editing_layer: null, _get_geometries_next_time: false, _geometries: {}, + _geometries_shadows: {}, _creating: false, _editing: null, _geometry_types: [], @@ -232,6 +233,7 @@ editor = { get_geometries: function () { // reload geometries of current level editor._geometries = {}; + editor._geometries_shadows = {}; if (editor._geometries_layer !== null) { editor.map.removeLayer(editor._geometries_layer); } @@ -263,24 +265,41 @@ editor = { 'door': '#FF00FF', 'hole': '#66CC66', 'elevatorlevel': '#9EF8FB', - 'levelconnector': '#FFFF00' + 'levelconnector': '#FFFF00', + 'shadow': '#000000', + 'stair': '#FF0000' + }, + _line_draw_geometry_style: function(style) { + style.stroke = true; + style.opacity = 0.6; + style.color = style.fillColor; + style.weight = 5; + return style; }, _get_geometry_style: function (feature) { // style callback for GeoJSON loader - return editor._get_mapitem_type_style(feature.properties.type); + var style = editor._get_mapitem_type_style(feature.properties.type); + if (feature.geometry.type == 'LineString') { + style = editor._line_draw_geometry_style(style); + } + return style }, _get_mapitem_type_style: function (mapitem_type) { // get styles for a specific mapitem return { + stroke: false, fillColor: editor._geometry_colors[mapitem_type], - weight: 0, fillOpacity: 0.6, smoothFactor: 0 }; }, _register_geojson_feature: function (feature, layer) { // onEachFeature callback for GeoJSON loader – register all needed events - editor._geometries[feature.properties.type+'-'+feature.properties.name] = layer; + if (feature.properties.type == 'shadow') { + editor._geometries_shadows[feature.properties.original_type+'-'+feature.properties.original_name] = layer; + } else { + editor._geometries[feature.properties.type+'-'+feature.properties.name] = layer; + } layer.on('mouseover', editor._hover_geometry_layer) .on('mouseout', editor._unhighlight_geometry) .on('click', editor._click_geometry_layer) @@ -361,6 +380,10 @@ editor = { var name = form.attr('data-name'); var pk = mapitem_type+'-'+name; editor._geometries_layer.removeLayer(editor._geometries[pk]); + var shadow = editor._geometries_shadows[pk]; + if (shadow) { + editor._geometries_layer.removeLayer(shadow); + } } editor._editing = L.geoJSON({ @@ -384,6 +407,7 @@ editor = { if (geomtype == 'polygon') { editor.map.editTools.startPolygon(null, options); } else if (geomtype == 'polyline') { + options = editor._line_draw_geometry_style(options); editor.map.editTools.startPolyline(null, options); } editor._creating = true; diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 7c23dcda..3eb5cb68 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -10,6 +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.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, @@ -92,7 +93,10 @@ class GeometryViewSet(ViewSet): if hasattr(mapitemtype, field_name): queryset.prefetch_related(field_name) - results.extend(sum((obj.to_geojson() for obj in queryset), [])) + if issubclass(mapitemtype, LineGeometryMapItemWithLevel): + results.extend(obj.to_shadow_geojson() for obj in queryset) + + results.extend(obj.to_geojson() for obj in queryset) return Response(results) diff --git a/src/c3nav/mapdata/migrations/0013_auto_20161208_0937.py b/src/c3nav/mapdata/migrations/0013_auto_20161208_0937.py new file mode 100644 index 00000000..9e664c8f --- /dev/null +++ b/src/c3nav/mapdata/migrations/0013_auto_20161208_0937.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-08 09:37 +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', '0012_auto_20161204_1544'), + ] + + operations = [ + migrations.CreateModel( + name='Stair', + 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()), + ], + options={ + 'default_related_name': 'stairs', + 'verbose_name_plural': 'Stairs', + 'verbose_name': 'Stair', + }, + ), + migrations.AlterModelOptions( + name='level', + options={'ordering': ['altitude'], 'verbose_name': 'Level', 'verbose_name_plural': 'Levels'}, + ), + migrations.AddField( + model_name='stair', + name='level', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stairs', to='mapdata.Level', verbose_name='level'), + ), + migrations.AddField( + model_name='stair', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stairs', to='mapdata.Package', verbose_name='map package'), + ), + ] diff --git a/src/c3nav/mapdata/models/geometry.py b/src/c3nav/mapdata/models/geometry.py index 62c5e776..d64c0b97 100644 --- a/src/c3nav/mapdata/models/geometry.py +++ b/src/c3nav/mapdata/models/geometry.py @@ -1,8 +1,8 @@ from collections import OrderedDict -from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ +from shapely.geometry import CAP_STYLE, JOIN_STYLE from shapely.geometry.geo import mapping, shape from c3nav.mapdata.fields import GeometryField @@ -26,7 +26,6 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta): A map feature """ geometry = GeometryField() - cached_geojson = {} geomtype = None @@ -50,12 +49,6 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta): return kwargs - @classmethod - def get_styles(cls): - return { - cls.__name__.lower(): cls.color - } - def get_geojson_properties(self): return OrderedDict(( ('type', self.__class__.__name__.lower()), @@ -64,25 +57,20 @@ class GeometryMapItem(MapItem, metaclass=GeometryMapItemMeta): )) def to_geojson(self): - if settings.DIRECT_EDITING: - return self._to_geojson() - key = (self.__class__, self.name) - if key not in self.cached_geojson: - self.cached_geojson[key] = self._to_geojson() - return self.cached_geojson[key] - - def _to_geojson(self): - return [OrderedDict(( + return OrderedDict(( ('type', 'Feature'), ('properties', self.get_geojson_properties()), ('geometry', format_geojson(mapping(self.geometry), round=False)), - ))] + )) def tofile(self): result = super().tofile() result['geometry'] = format_geojson(mapping(self.geometry)) return result + def get_shadow_geojson(self): + return None + class GeometryMapItemWithLevel(GeometryMapItem): """ @@ -289,3 +277,42 @@ class ElevatorLevel(GeometryMapItemWithLevel): result['elevator'] = self.elevator.name result['button'] = self.button return result + + +class LineGeometryMapItemWithLevel(GeometryMapItemWithLevel): + geomtype = 'polyline' + + class Meta: + abstract = True + + def to_geojson(self): + result = super().to_geojson() + original_geometry = result['geometry'] + draw = self.geometry.buffer(0.05, join_style=JOIN_STYLE.mitre, cap_style=CAP_STYLE.flat) + result['geometry'] = format_geojson(mapping(draw)) + result['original_geometry'] = original_geometry + return result + + def to_shadow_geojson(self): + shadow = self.geometry.parallel_offset(0.03, 'left', join_style=JOIN_STYLE.mitre) + shadow = shadow.buffer(0.019, join_style=JOIN_STYLE.mitre, cap_style=CAP_STYLE.flat) + return OrderedDict(( + ('type', 'Feature'), + ('properties', OrderedDict(( + ('type', 'shadow'), + ('original_type', self.__class__.__name__.lower()), + ('original_name', self.name), + ('level', self.level.name), + ))), + ('geometry', format_geojson(mapping(shadow), round=False)), + )) + + +class Stair(LineGeometryMapItemWithLevel): + """ + A stair + """ + class Meta: + verbose_name = _('Stair') + verbose_name_plural = _('Stairs') + default_related_name = 'stairs' diff --git a/src/c3nav/mapdata/models/level.py b/src/c3nav/mapdata/models/level.py index e25afb84..79c4930a 100644 --- a/src/c3nav/mapdata/models/level.py +++ b/src/c3nav/mapdata/models/level.py @@ -5,6 +5,7 @@ from shapely.geometry import JOIN_STYLE from shapely.ops import cascaded_union from c3nav.mapdata.models.base import MapItem +from c3nav.mapdata.utils.geometry import assert_multilinestring class Level(MapItem): @@ -193,3 +194,14 @@ class LevelGeometries(): shadows = shadows.difference(connectors.buffer(1.0, join_style=JOIN_STYLE.mitre)) return shadows + + @cached_property + def stairs(self): + return cascaded_union([stair.geometry for stair in self.level.stairs.all()]) + + @cached_property + def stair_shadows(self): + shadows = [] + for stair in assert_multilinestring(self.stairs): + shadows.append(stair.parallel_offset(0.1, 'left', join_style=JOIN_STYLE.mitre)) + return cascaded_union(shadows) diff --git a/src/c3nav/mapdata/render/renderer.py b/src/c3nav/mapdata/render/renderer.py index dc7bc1c7..628129f6 100644 --- a/src/c3nav/mapdata/render/renderer.py +++ b/src/c3nav/mapdata/render/renderer.py @@ -32,7 +32,8 @@ class LevelRenderer(): return (width * settings.RENDER_SCALE, height * settings.RENDER_SCALE) @staticmethod - def polygon_svg(geometry, fill_color=None, fill_opacity=None, stroke_width=0.0, stroke_color=None, filter=None): + def polygon_svg(geometry, fill_color=None, fill_opacity=None, + stroke_width=0.0, stroke_color=None, stroke_opacity=None): scaled = scale(geometry, xfact=settings.RENDER_SCALE, yfact=settings.RENDER_SCALE, origin=(0, 0)) element = ET.fromstring(scaled.svg(0, fill_color or '#FFFFFF')) if element.tag != 'g': @@ -40,7 +41,11 @@ class LevelRenderer(): new_element.append(element) element = new_element - for path in element.findall('path'): + paths = element.findall('polyline') + if len(paths) == 0: + paths = element.findall('path') + + for path in paths: path.attrib.pop('opacity') path.set('stroke-width', str(stroke_width * settings.RENDER_SCALE)) @@ -56,8 +61,8 @@ class LevelRenderer(): elif 'stroke' in path.attrib: path.attrib.pop('stroke') - if filter is not None: - path.set('filter', filter) + if stroke_opacity is not None: + path.set('stroke-opacity', str(stroke_opacity)) return element @@ -107,6 +112,16 @@ class LevelRenderer(): contents.append(self.polygon_svg(self.level.geometries.outsides_with_holes, fill_color='#DCE6DC')) + contents.append(self.polygon_svg(self.level.geometries.stair_shadows, + stroke_color='#000000', + stroke_width=0.1, + stroke_opacity=0.1)) + + contents.append(self.polygon_svg(self.level.geometries.stairs, + stroke_color='#000000', + stroke_width=0.06, + stroke_opacity=0.2)) + contents.append(self.polygon_svg(self.level.geometries.walls_shadow, fill_color='#000000', fill_opacity=0.06)) diff --git a/src/c3nav/mapdata/utils/geometry.py b/src/c3nav/mapdata/utils/geometry.py index c09eb314..bc0d92a6 100644 --- a/src/c3nav/mapdata/utils/geometry.py +++ b/src/c3nav/mapdata/utils/geometry.py @@ -1,4 +1,4 @@ -from shapely.geometry import Polygon +from shapely.geometry import LineString, Polygon def clean_geometry(geometry): @@ -26,7 +26,16 @@ def assert_multipolygon(geometry): :return: a list of Polygons """ if isinstance(geometry, Polygon): - polygons = [geometry] - else: - polygons = geometry.geoms - return polygons + return [geometry] + return geometry.geoms + + +def assert_multilinestring(geometry): + """ + given a Geometry or GeometryCollection, return a list of Geometries + :param geometry: a Geometry or a GeometryCollection + :return: a list of Geometries + """ + if isinstance(geometry, LineString): + return [geometry] + return geometry.geoms