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.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):
"""

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 */
.leaflet-container {
background:#000000;
}
.leaflet-control-layers-overlays label {
margin-bottom:0;
}

View file

@ -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<sections.length;i++) {
var section = $(sections[i]);
editor._section_control.addSection(section.text(), section.attr('href'), section.is('.current'));
}
if (sections.length > 1) {
editor._section_control.enable();
var sections = content.find('[data-sections] a');
if (sections.length) {
for(var i=0;i<sections.length;i++) {
var section = $(sections[i]);
editor._section_control.addSection(section.text(), section.attr('href'), section.is('.current'));
}
if (sections.length > 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 += '&' + $('<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');
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;

View file

@ -11,3 +11,6 @@
<li><a href="" class="current">{{ section.title }}</a></li>
</ul>
{% 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)
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({

View file

@ -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=<id> to filter by section. """

View file

@ -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):

View file

@ -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

View file

@ -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)