show geometries in editor… introducing the editor API

This commit is contained in:
Laura Klünder 2017-05-21 23:39:26 +02:00
parent 8c4540c656
commit 46dc1627e1
10 changed files with 167 additions and 73 deletions

View file

@ -7,6 +7,7 @@ from rest_framework.generics import GenericAPIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import SimpleRouter from rest_framework.routers import SimpleRouter
from c3nav.editor.api import EditorViewSet
from c3nav.mapdata.api import (AreaViewSet, BuildingViewSet, DoorViewSet, HoleViewSet, LineObstacleViewSet, from c3nav.mapdata.api import (AreaViewSet, BuildingViewSet, DoorViewSet, HoleViewSet, LineObstacleViewSet,
LocationGroupViewSet, LocationViewSet, ObstacleViewSet, PointViewSet, SectionViewSet, LocationGroupViewSet, LocationViewSet, ObstacleViewSet, PointViewSet, SectionViewSet,
SourceViewSet, SpaceViewSet, StairViewSet) SourceViewSet, SpaceViewSet, StairViewSet)
@ -27,6 +28,8 @@ router.register(r'sources', SourceViewSet)
router.register(r'locations', LocationViewSet) router.register(r'locations', LocationViewSet)
router.register(r'locationgroups', LocationGroupViewSet) router.register(r'locationgroups', LocationGroupViewSet)
router.register(r'editor', EditorViewSet, base_name='editor')
class APIRoot(GenericAPIView): class APIRoot(GenericAPIView):
""" """

92
src/c3nav/editor/api.py Normal file
View file

@ -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=<id> or ?space=<id>
/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',
})

View file

@ -116,6 +116,9 @@ form button.invisiblesubmit {
} }
/* Styles inside leaflet */ /* Styles inside leaflet */
.leaflet-container {
background:#000000;
}
.leaflet-control-layers-overlays label { .leaflet-control-layers-overlays label {
margin-bottom:0; margin-bottom:0;
} }

View file

@ -40,7 +40,6 @@ editor = {
editor._section_control = new SectionControl().addTo(editor.map); editor._section_control = new SectionControl().addTo(editor.map);
editor.init_geometries(); editor.init_geometries();
editor.init_sidebar();
editor.get_sources(); editor.get_sources();
var bounds = [[0.0, 0.0], [240.0, 400.0]]; var bounds = [[0.0, 0.0], [240.0, 400.0]];
@ -114,21 +113,27 @@ editor = {
return; return;
} }
sections = content.find('[data-sections]'); var geometry_url = content.find('[data-geometry-url]');
if (sections.length) { if (geometry_url.length) {
geometry_url = geometry_url.attr('data-geometry-url');
editor.load_geometries(geometry_url);
$('body').addClass('map-enabled'); $('body').addClass('map-enabled');
var sections = sections.find('a');
editor._section_control.clearSections(); editor._section_control.clearSections();
for(var i=0;i<sections.length;i++) { var sections = content.find('[data-sections] a');
var section = $(sections[i]); if (sections.length) {
editor._section_control.addSection(section.text(), section.attr('href'), section.is('.current')); for(var i=0;i<sections.length;i++) {
} var section = $(sections[i]);
if (sections.length > 1) { editor._section_control.addSection(section.text(), section.attr('href'), section.is('.current'));
editor._section_control.enable(); }
if (sections.length > 1) {
editor._section_control.enable();
} else {
editor._section_control.disable();
}
editor._section_control.show()
} else { } else {
editor._section_control.disable(); editor._section_control.hide();
} }
editor._section_control.show()
} else { } else {
$('body').removeClass('map-enabled').removeClass('show-map'); $('body').removeClass('map-enabled').removeClass('show-map');
editor._section_control.hide(); editor._section_control.hide();
@ -160,9 +165,6 @@ editor = {
if ($(btn).is('[name]')) { if ($(btn).is('[name]')) {
data += '&' + $('<input>').attr('name', $(btn).attr('name')).val($(btn).val()).serialize(); data += '&' + $('<input>').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'); var action = $(this).attr('action');
editor._sidebar_unload(); editor._sidebar_unload();
@ -170,10 +172,10 @@ editor = {
}, },
// geometries // geometries
geometrystyles: {},
_geometries_layer: null, _geometries_layer: null,
_highlight_layer: null, _highlight_layer: null,
_editing_layer: null, _editing_layer: null,
_get_geometries_next_time: false,
_geometries: {}, _geometries: {},
_geometries_shadows: {}, _geometries_shadows: {},
_creating: false, _creating: false,
@ -195,15 +197,20 @@ editor = {
editor.map.on('editable:vertex:ctrlclick editable:vertex:metakeyclick', function (e) { editor.map.on('editable:vertex:ctrlclick editable:vertex:metakeyclick', function (e) {
e.vertex.continue(); e.vertex.continue();
}); });
$.getJSON('/api/editor/geometrystyles/', function(geometrystyles) {
editor.geometrystyles = geometrystyles;
editor.init_sidebar();
});
}, },
get_geometries: function () { load_geometries: function (geometry_url) {
// reload geometries of current level // load geometries from url
editor._geometries = {}; editor._geometries = {};
editor._geometries_shadows = {}; editor._geometries_shadows = {};
if (editor._geometries_layer !== null) { if (editor._geometries_layer !== null) {
editor.map.removeLayer(editor._geometries_layer); 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, { editor._geometries_layer = L.geoJSON(geometries, {
style: editor._get_geometry_style, style: editor._get_geometry_style,
onEachFeature: editor._register_geojson_feature onEachFeature: editor._register_geojson_feature
@ -213,21 +220,6 @@ editor = {
editor._loading_geometry = false; 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) { _line_draw_geometry_style: function(style) {
style.stroke = true; style.stroke = true;
style.opacity = 0.6; style.opacity = 0.6;
@ -241,22 +233,19 @@ editor = {
if (feature.geometry.type === 'LineString') { if (feature.geometry.type === 'LineString') {
style = editor._line_draw_geometry_style(style); style = editor._line_draw_geometry_style(style);
} }
if (feature.properties.color !== undefined) {
style.fillColor = feature.properties.color;
}
return style return style
}, },
_get_mapitem_type_style: function (mapitem_type) { _get_mapitem_type_style: function (mapitem_type) {
// get styles for a specific mapitem // get styles for a specific mapitem
var result = { var result = {
stroke: false, stroke: false,
fillColor: editor._geometry_colors[mapitem_type], fillColor: editor.geometrystyles[mapitem_type],
fillOpacity: (mapitem_type === 'arealocation') ? 0.2 : 0.6, fillOpacity: 1,
smoothFactor: 0 smoothFactor: 0
}; };
if (mapitem_type === 'arealocation') {
result.fillOpacity = 0.02;
result.color = result.fillColor;
result.stroke = true;
result.weight = 1;
}
return result; return result;
}, },
_register_geojson_feature: function (feature, layer) { _register_geojson_feature: function (feature, layer) {
@ -376,9 +365,6 @@ editor = {
$('#id_level').val(editor._level); $('#id_level').val(editor._level);
$('#id_levels').find('option[value='+editor._level+']').prop('selected', true); $('#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() { _cancel_editing: function() {
@ -387,7 +373,6 @@ editor = {
editor._editing_layer.clearLayers(); editor._editing_layer.clearLayers();
editor._editing.disableEdit(); editor._editing.disableEdit();
editor._editing = null; editor._editing = null;
editor._get_geometries_next_time = true;
} }
if (editor._creating) { if (editor._creating) {
editor._creating = false; editor._creating = false;

View file

@ -11,3 +11,6 @@
<li><a href="" class="current">{{ section.title }}</a></li> <li><a href="" class="current">{{ section.title }}</a></li>
</ul> </ul>
{% endif %} {% endif %}
{% if geometry_url %}
<span data-geometry-url="{{ geometry_url }}"></span>
{% endif %}

View file

@ -57,6 +57,7 @@ def section_detail(request, pk):
'child_models': [child_model(model_name, kwargs={'section': pk}, parent=section) 'child_models': [child_model(model_name, kwargs={'section': pk}, parent=section)
for model_name in ('Building', 'Space', 'Door', 'Hole')], 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) 'child_models': [child_model(model_name, kwargs={'space': pk}, parent=space)
for model_name in ('Area', 'Stair', 'Obstacle', 'LineObstacle', 'Point')], 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({ ctx.update({
'section': obj, 'section': obj,
'back_url': reverse('editor.index') if new else reverse('editor.sections.detail', kwargs={'pk': pk}), '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: elif model == Space and not new:
ctx.update({ ctx.update({
'section': obj, 'section': obj,
'back_url': reverse('editor.spaces.detail', kwargs={'section': obj.section.pk, 'pk': pk}), 'back_url': reverse('editor.spaces.detail', kwargs={'section': obj.section.pk, 'pk': pk}),
'geometry_url': '/api/editor/geometries/?space='+pk,
}) })
elif hasattr(obj, 'section'): elif hasattr(obj, 'section'):
ctx.update({ ctx.update({
'section': obj.section, 'section': obj.section,
'back_url': reverse('editor.sections.detail', kwargs={'pk': obj.section.pk}), 'back_url': reverse('editor.sections.detail', kwargs={'pk': obj.section.pk}),
'geometry_url': '/api/editor/geometries/?section='+pk,
}) })
elif hasattr(obj, 'space'): elif hasattr(obj, 'space'):
ctx.update({ ctx.update({
'section': obj.space.section, 'section': obj.space.section,
'back_url': reverse('editor.spaces.detail', kwargs={'section': obj.space.section.pk, 'pk': obj.space.pk}), 'back_url': reverse('editor.spaces.detail', kwargs={'section': obj.space.section.pk, 'pk': obj.space.pk}),
'geometry_url': '/api/editor/geometries/?space='+pk,
}) })
else: else:
kwargs = {} kwargs = {}
@ -200,6 +206,7 @@ def list_objects(request, model=None, section=None, space=None, explicit_edit=Fa
'sections': Section.objects.all(), 'sections': Section.objects.all(),
'section': section, 'section': section,
'section_url': request.resolver_match.url_name, 'section_url': request.resolver_match.url_name,
'geometry_url': '/api/editor/geometries/?section='+str(section.pk),
}) })
elif space is not None: elif space is not None:
reverse_kwargs['space'] = space reverse_kwargs['space'] = space
@ -209,6 +216,7 @@ def list_objects(request, model=None, section=None, space=None, explicit_edit=Fa
'section': space.section, 'section': space.section,
'back_url': reverse('editor.spaces.detail', kwargs={'section': space.section.pk, 'pk': space.pk}), 'back_url': reverse('editor.spaces.detail', kwargs={'section': space.section.pk, 'pk': space.pk}),
'back_title': _('back to space'), 'back_title': _('back to space'),
'geometry_url': '/api/editor/geometries/?space='+str(space.pk),
}) })
else: else:
ctx.update({ ctx.update({

View file

@ -57,18 +57,6 @@ class SectionViewSet(MapdataViewSet):
def geometrytypes(self, request): def geometrytypes(self, request):
return self.list_types(SECTION_MODELS) 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']) @detail_route(methods=['get'])
def svg(self, requests, pk=None): def svg(self, requests, pk=None):
section = self.get_object() section = self.get_object()
@ -89,18 +77,6 @@ class SpaceViewSet(MapdataViewSet):
def geometrytypes(self, request): def geometrytypes(self, request):
return self.list_types(SPACE_MODELS) 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): class DoorViewSet(MapdataViewSet):
""" Add ?geometry=1 to get geometries, add ?section=<id> to filter by section. """ """ Add ?geometry=1 to get geometries, add ?section=<id> to filter by section. """

View file

@ -17,6 +17,10 @@ class SectionGeometryMixin(GeometryMixin):
def get_geojson_properties(self) -> dict: def get_geojson_properties(self) -> dict:
result = super().get_geojson_properties() result = super().get_geojson_properties()
result['layer'] = getattr(self, 'level', 'base') result['layer'] = getattr(self, 'level', 'base')
if hasattr(self, 'get_color'):
color = self.get_color()
if color:
result['color'] = color
return result return result
def _serialize(self, section=True, **kwargs): def _serialize(self, section=True, **kwargs):

View file

@ -24,6 +24,14 @@ class SpaceGeometryMixin(GeometryMixin):
result['space'] = self.space.id result['space'] = self.space.id
return result 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): class Area(SpecificLocation, SpaceGeometryMixin, models.Model):
""" """
@ -140,3 +148,13 @@ class Point(SpecificLocation, SpaceGeometryMixin, models.Model):
verbose_name = _('Point') verbose_name = _('Point')
verbose_name_plural = _('Points') verbose_name_plural = _('Points')
default_related_name = '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

View file

@ -63,7 +63,7 @@ class Section(SpecificLocation, EditorFormMixin, models.Model):
).intersection(space.geometry) ).intersection(space.geometry)
svg.add_geometry(obstacle_geometries, fill_color='#999999') 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() width, height = get_dimensions()
svg = SVGImage(width=width, height=height, scale=settings.RENDER_SCALE) 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) wall_geometry = building_geometries.difference(space_geometries['']).difference(door_geometries)
# draw wall shadow # draw wall shadow
wall_dilated_geometry = wall_geometry.buffer(0.7, join_style=JOIN_STYLE.mitre) if effects:
svg.add_geometry(wall_dilated_geometry, fill_color='#000000', opacity=0.1, filter='wallblur', clip_path=section_clip) 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['']: for space in space_levels['']:
self._render_space_inventory(svg, space) self._render_space_inventory(svg, space)