diff --git a/src/c3nav/api/urls.py b/src/c3nav/api/urls.py index c09cf4ed..ddf0afac 100644 --- a/src/c3nav/api/urls.py +++ b/src/c3nav/api/urls.py @@ -7,6 +7,7 @@ from rest_framework.generics import GenericAPIView from rest_framework.response import Response from rest_framework.routers import SimpleRouter +from c3nav.editor.api import EditorViewSet from c3nav.mapdata.api import (AreaViewSet, BuildingViewSet, DoorViewSet, HoleViewSet, LineObstacleViewSet, LocationGroupViewSet, LocationViewSet, ObstacleViewSet, PointViewSet, SectionViewSet, SourceViewSet, SpaceViewSet, StairViewSet) @@ -27,6 +28,8 @@ router.register(r'sources', SourceViewSet) router.register(r'locations', LocationViewSet) router.register(r'locationgroups', LocationGroupViewSet) +router.register(r'editor', EditorViewSet, base_name='editor') + class APIRoot(GenericAPIView): """ diff --git a/src/c3nav/editor/api.py b/src/c3nav/editor/api.py new file mode 100644 index 00000000..0df0fece --- /dev/null +++ b/src/c3nav/editor/api.py @@ -0,0 +1,92 @@ +from itertools import chain + +from django.utils import timezone +from rest_framework.decorators import detail_route, list_route +from rest_framework.exceptions import ValidationError +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response +from rest_framework.viewsets import ViewSet +from shapely.ops import cascaded_union + +from c3nav.mapdata.models import Area, Section, Space + + +class EditorViewSet(ViewSet): + """ + Editor API + /geometries/ returns a list of geojson features, you have to specify ?section= or ?space= + /geometrystyles/ returns styling information for all geometry types + """ + @list_route(methods=['get']) + def geometries(self, request, *args, **kwargs): + section = request.GET.get('section') + space = request.GET.get('space') + if section is not None: + if space is not None: + raise ValidationError('Only section or space can be specified.') + section = get_object_or_404(Section, pk=section) + holes = section.holes.all() + holes_geom = cascaded_union([hole.geometry for hole in holes]) + buildings = section.buildings.all() + spaces = section.spaces.all() + spaces_geom = cascaded_union([space.geometry for space in spaces if space.level == '']) + holes_geom = holes_geom.intersection(spaces_geom) + doors = section.doors.all() + for obj in chain(buildings, (s for s in spaces if s.level == '')): + obj.geometry = obj.geometry.difference(holes_geom) + + results = [] + + def add_spaces(level): + results.extend(space for space in spaces if space.level == level) + results.extend((area for area in Area.objects.filter(space__section=section, space__level=level) + if area.get_color())) + + add_spaces('lower') + + results.extend(buildings) + for door in section.doors.all(): + results.append(door) + + add_spaces('') + add_spaces('upper') + return Response([obj.to_geojson() for obj in results]) + elif space is not None: + space = get_object_or_404(Space, pk=space) + section = space.section + + doors = [door for door in section.doors.all() if door.geometry.intersects(space.geometry)] + doors_geom = cascaded_union([door.geometry for door in doors]) + + spaces = [space for space in section.spaces.all() if space.geometry.intersects(doors_geom)] + + results = [] + results.extend(section.buildings.all()) + results.extend(doors) + results.extend(spaces) + + results.extend(chain( + space.areas.all(), + space.stairs.all(), + space.obstacles.all(), + space.lineobstacles.all(), + space.points.all(), + )) + + return Response([obj.to_geojson() for obj in results]) + else: + raise ValidationError('No section or space specified.') + + @list_route(methods=['get']) + def geometrystyles(self, request, *args, **kwargs): + return Response({ + 'building': '#929292', + 'space': '#d1d1d1', + 'hole': 'rgba(255, 0, 0, 0.3)', + 'door': '#ffffff', + 'area': '#55aaff', + 'step': '#ff0099', + 'obstacle': '#999999', + 'lineobstacle': '#999999', + 'point': '#4488cc', + }) diff --git a/src/c3nav/editor/static/editor/css/editor.css b/src/c3nav/editor/static/editor/css/editor.css index c374f25b..7359f543 100644 --- a/src/c3nav/editor/static/editor/css/editor.css +++ b/src/c3nav/editor/static/editor/css/editor.css @@ -116,6 +116,9 @@ form button.invisiblesubmit { } /* Styles inside leaflet */ +.leaflet-container { + background:#000000; +} .leaflet-control-layers-overlays label { margin-bottom:0; } diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index 468b8a5a..6fff0fdb 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -40,7 +40,6 @@ editor = { editor._section_control = new SectionControl().addTo(editor.map); editor.init_geometries(); - editor.init_sidebar(); editor.get_sources(); var bounds = [[0.0, 0.0], [240.0, 400.0]]; @@ -114,21 +113,27 @@ editor = { return; } - sections = content.find('[data-sections]'); - if (sections.length) { + var geometry_url = content.find('[data-geometry-url]'); + if (geometry_url.length) { + geometry_url = geometry_url.attr('data-geometry-url'); + editor.load_geometries(geometry_url); $('body').addClass('map-enabled'); - var sections = sections.find('a'); editor._section_control.clearSections(); - for(var i=0;i 1) { - editor._section_control.enable(); + var sections = content.find('[data-sections] a'); + if (sections.length) { + for(var i=0;i 1) { + editor._section_control.enable(); + } else { + editor._section_control.disable(); + } + editor._section_control.show() } else { - editor._section_control.disable(); + editor._section_control.hide(); } - editor._section_control.show() } else { $('body').removeClass('map-enabled').removeClass('show-map'); editor._section_control.hide(); @@ -160,9 +165,6 @@ editor = { if ($(btn).is('[name]')) { data += '&' + $('').attr('name', $(btn).attr('name')).val($(btn).val()).serialize(); } - if ($(btn).is('[data-reload-geometries]')) { - editor._get_geometries_next_time = true; - } } var action = $(this).attr('action'); editor._sidebar_unload(); @@ -170,10 +172,10 @@ editor = { }, // geometries + geometrystyles: {}, _geometries_layer: null, _highlight_layer: null, _editing_layer: null, - _get_geometries_next_time: false, _geometries: {}, _geometries_shadows: {}, _creating: false, @@ -195,15 +197,20 @@ editor = { editor.map.on('editable:vertex:ctrlclick editable:vertex:metakeyclick', function (e) { e.vertex.continue(); }); + + $.getJSON('/api/editor/geometrystyles/', function(geometrystyles) { + editor.geometrystyles = geometrystyles; + editor.init_sidebar(); + }); }, - get_geometries: function () { - // reload geometries of current level + load_geometries: function (geometry_url) { + // load geometries from url editor._geometries = {}; editor._geometries_shadows = {}; if (editor._geometries_layer !== null) { editor.map.removeLayer(editor._geometries_layer); } - $.getJSON('/api/geometries/?level='+String(editor._level), function(geometries) { + $.getJSON(geometry_url, function(geometries) { editor._geometries_layer = L.geoJSON(geometries, { style: editor._get_geometry_style, onEachFeature: editor._register_geojson_feature @@ -213,21 +220,6 @@ editor = { editor._loading_geometry = false; }); }, - _geometry_colors: { - 'building': '#333333', - 'area': '#FFFFFF', - 'lineobstacle': '#999999', - 'obstacle': '#999999', - 'door': '#66FF00', - 'hole': '#66CC99', - 'elevatorlevel': '#9EF8FB', - 'levelconnector': '#FFFF00', - 'shadow': '#000000', - 'stair': '#FF0000', - 'arealocation': '#0099FF', - 'escalator': '#FF9900', - 'stuffedarea': '#D9A3A3' - }, _line_draw_geometry_style: function(style) { style.stroke = true; style.opacity = 0.6; @@ -241,22 +233,19 @@ editor = { if (feature.geometry.type === 'LineString') { style = editor._line_draw_geometry_style(style); } + if (feature.properties.color !== undefined) { + style.fillColor = feature.properties.color; + } return style }, _get_mapitem_type_style: function (mapitem_type) { // get styles for a specific mapitem var result = { stroke: false, - fillColor: editor._geometry_colors[mapitem_type], - fillOpacity: (mapitem_type === 'arealocation') ? 0.2 : 0.6, + fillColor: editor.geometrystyles[mapitem_type], + fillOpacity: 1, smoothFactor: 0 }; - if (mapitem_type === 'arealocation') { - result.fillOpacity = 0.02; - result.color = result.fillColor; - result.stroke = true; - result.weight = 1; - } return result; }, _register_geojson_feature: function (feature, layer) { @@ -376,9 +365,6 @@ editor = { $('#id_level').val(editor._level); $('#id_levels').find('option[value='+editor._level+']').prop('selected', true); } - } else if (editor._get_geometries_next_time) { - editor.get_geometries(); - editor._get_geometries_next_time = false; } }, _cancel_editing: function() { @@ -387,7 +373,6 @@ editor = { editor._editing_layer.clearLayers(); editor._editing.disableEdit(); editor._editing = null; - editor._get_geometries_next_time = true; } if (editor._creating) { editor._creating = false; diff --git a/src/c3nav/editor/templates/editor/fragment_sections.html b/src/c3nav/editor/templates/editor/fragment_sections.html index ccbaaa7e..629c4594 100644 --- a/src/c3nav/editor/templates/editor/fragment_sections.html +++ b/src/c3nav/editor/templates/editor/fragment_sections.html @@ -11,3 +11,6 @@
  • {{ section.title }}
  • {% endif %} +{% if geometry_url %} + +{% endif %} diff --git a/src/c3nav/editor/views.py b/src/c3nav/editor/views.py index 6ed86f92..4f39f85e 100644 --- a/src/c3nav/editor/views.py +++ b/src/c3nav/editor/views.py @@ -57,6 +57,7 @@ def section_detail(request, pk): 'child_models': [child_model(model_name, kwargs={'section': pk}, parent=section) for model_name in ('Building', 'Space', 'Door', 'Hole')], + 'geometry_url': '/api/editor/geometries/?section='+pk, }) @@ -70,6 +71,7 @@ def space_detail(request, section, pk): 'child_models': [child_model(model_name, kwargs={'space': pk}, parent=space) for model_name in ('Area', 'Stair', 'Obstacle', 'LineObstacle', 'Point')], + 'geometry_url': '/api/editor/geometries/?space='+pk, }) @@ -103,21 +105,25 @@ def edit(request, pk=None, model=None, section=None, space=None, explicit_edit=F ctx.update({ 'section': obj, 'back_url': reverse('editor.index') if new else reverse('editor.sections.detail', kwargs={'pk': pk}), + 'geometry_url': '/api/editor/geometries/?section='+pk, }) elif model == Space and not new: ctx.update({ 'section': obj, 'back_url': reverse('editor.spaces.detail', kwargs={'section': obj.section.pk, 'pk': pk}), + 'geometry_url': '/api/editor/geometries/?space='+pk, }) elif hasattr(obj, 'section'): ctx.update({ 'section': obj.section, 'back_url': reverse('editor.sections.detail', kwargs={'pk': obj.section.pk}), + 'geometry_url': '/api/editor/geometries/?section='+pk, }) elif hasattr(obj, 'space'): ctx.update({ 'section': obj.space.section, 'back_url': reverse('editor.spaces.detail', kwargs={'section': obj.space.section.pk, 'pk': obj.space.pk}), + 'geometry_url': '/api/editor/geometries/?space='+pk, }) else: kwargs = {} @@ -200,6 +206,7 @@ def list_objects(request, model=None, section=None, space=None, explicit_edit=Fa 'sections': Section.objects.all(), 'section': section, 'section_url': request.resolver_match.url_name, + 'geometry_url': '/api/editor/geometries/?section='+str(section.pk), }) elif space is not None: reverse_kwargs['space'] = space @@ -209,6 +216,7 @@ def list_objects(request, model=None, section=None, space=None, explicit_edit=Fa 'section': space.section, 'back_url': reverse('editor.spaces.detail', kwargs={'section': space.section.pk, 'pk': space.pk}), 'back_title': _('back to space'), + 'geometry_url': '/api/editor/geometries/?space='+str(space.pk), }) else: ctx.update({ diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 03e2a7e3..1f4de0d4 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -57,18 +57,6 @@ class SectionViewSet(MapdataViewSet): def geometrytypes(self, request): return self.list_types(SECTION_MODELS) - @detail_route(methods=['get']) - def geometries(self, requests, pk=None): - section = self.get_object() - results = [] - results.extend(section.buildings.all()) - results.extend(section.holes.all()) - for space in section.spaces.all(): - results.append(space) - for door in section.doors.all(): - results.append(door) - return Response([obj.to_geojson() for obj in results]) - @detail_route(methods=['get']) def svg(self, requests, pk=None): section = self.get_object() @@ -89,18 +77,6 @@ class SpaceViewSet(MapdataViewSet): def geometrytypes(self, request): return self.list_types(SPACE_MODELS) - @detail_route(methods=['get']) - def geometries(self, requests, pk=None): - space = self.get_object() - results = chain( - space.stairs.all(), - space.areas.all(), - space.obstacles.all(), - space.lineobstacles.all(), - space.points.all(), - ) - return Response([obj.to_geojson() for obj in results]) - class DoorViewSet(MapdataViewSet): """ Add ?geometry=1 to get geometries, add ?section= to filter by section. """ diff --git a/src/c3nav/mapdata/models/geometry/section.py b/src/c3nav/mapdata/models/geometry/section.py index a4d23811..51a9c5b9 100644 --- a/src/c3nav/mapdata/models/geometry/section.py +++ b/src/c3nav/mapdata/models/geometry/section.py @@ -17,6 +17,10 @@ class SectionGeometryMixin(GeometryMixin): def get_geojson_properties(self) -> dict: result = super().get_geojson_properties() result['layer'] = getattr(self, 'level', 'base') + if hasattr(self, 'get_color'): + color = self.get_color() + if color: + result['color'] = color return result def _serialize(self, section=True, **kwargs): diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index e95e339e..f85d23f2 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -24,6 +24,14 @@ class SpaceGeometryMixin(GeometryMixin): result['space'] = self.space.id return result + def get_geojson_properties(self) -> dict: + result = super().get_geojson_properties() + if hasattr(self, 'get_color'): + color = self.get_color() + if color: + result['color'] = color + return result + class Area(SpecificLocation, SpaceGeometryMixin, models.Model): """ @@ -140,3 +148,13 @@ class Point(SpecificLocation, SpaceGeometryMixin, models.Model): verbose_name = _('Point') verbose_name_plural = _('Points') default_related_name = 'points' + + @property + def buffered_geometry(self): + return self.geometry.buffer(0.5) + + def to_geojson(self): + result = super().to_geojson() + result['original_geometry'] = result['geometry'] + result['geometry'] = format_geojson(mapping(self.buffered_geometry)) + return result diff --git a/src/c3nav/mapdata/models/section.py b/src/c3nav/mapdata/models/section.py index 1f266683..e777596b 100644 --- a/src/c3nav/mapdata/models/section.py +++ b/src/c3nav/mapdata/models/section.py @@ -63,7 +63,7 @@ class Section(SpecificLocation, EditorFormMixin, models.Model): ).intersection(space.geometry) svg.add_geometry(obstacle_geometries, fill_color='#999999') - def render_svg(self): + def render_svg(self, effects=True, draw_spaces=None): width, height = get_dimensions() svg = SVGImage(width=width, height=height, scale=settings.RENDER_SCALE) @@ -120,8 +120,10 @@ class Section(SpecificLocation, EditorFormMixin, models.Model): wall_geometry = building_geometries.difference(space_geometries['']).difference(door_geometries) # draw wall shadow - wall_dilated_geometry = wall_geometry.buffer(0.7, join_style=JOIN_STYLE.mitre) - svg.add_geometry(wall_dilated_geometry, fill_color='#000000', opacity=0.1, filter='wallblur', clip_path=section_clip) + if effects: + wall_dilated_geometry = wall_geometry.buffer(0.7, join_style=JOIN_STYLE.mitre) + svg.add_geometry(wall_dilated_geometry, fill_color='#000000', opacity=0.1, filter='wallblur', + clip_path=section_clip) for space in space_levels['']: self._render_space_inventory(svg, space)