diff --git a/src/c3nav/api/urls.py b/src/c3nav/api/urls.py index 1a858435..c0d6753f 100644 --- a/src/c3nav/api/urls.py +++ b/src/c3nav/api/urls.py @@ -1,5 +1,4 @@ from django.conf.urls import include, url - from rest_framework.routers import DefaultRouter from ..editor import api as editor_api diff --git a/src/c3nav/editor/api.py b/src/c3nav/editor/api.py index f1f190eb..8df60c7e 100644 --- a/src/c3nav/editor/api.py +++ b/src/c3nav/editor/api.py @@ -1,5 +1,4 @@ from django.http import Http404 - from rest_framework.response import Response from rest_framework.viewsets import ViewSet diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index c26d1c43..f1550220 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -17,6 +17,7 @@ class FeatureForm(ModelForm): titles = OrderedDict((lang_code, '') for lang_code, language in settings.LANGUAGES) if self.instance is not None and self.instance.pk: + self.fields['name'].widget.attrs['readonly'] = True titles.update(self.instance.titles) language_titles = dict(settings.LANGUAGES) @@ -35,6 +36,14 @@ class FeatureForm(ModelForm): _('You have to select a title in at least one language.') ) + def clean_name(self): + if self.instance is not None and self.instance.pk and self.cleaned_data['name'] != self.instance.name: + raise ValidationError( + _('You cannot edit feature identifiers of existing objects.') + ) + + return self.cleaned_data['name'] + def get_languages(self): pass diff --git a/src/c3nav/editor/static/editor/css/editor.css b/src/c3nav/editor/static/editor/css/editor.css index c42c757b..b3942069 100644 --- a/src/c3nav/editor/static/editor/css/editor.css +++ b/src/c3nav/editor/static/editor/css/editor.css @@ -78,9 +78,46 @@ padding-left:5.5px; padding-right:5.5px; } +legend { + margin: 0; +} .start-drawing { display:none; } .leaflet-editable-drawing .leaflet-overlay-pane .leaflet-interactive { cursor:crosshair; } +.feature_level_list { + list-style-type:none; + display:none; + margin-bottom:15px; + padding:0; +} +.feature_level_list li { + padding:5px 0 0; + border-style:solid; + border-width:0 0 1px; + border-color:#EEEEEE; +} +.feature_level_list li:hover, .feature_level_list li.hover { + background-color:#FFFFEE; +} +.feature_level_list p { + margin:0 0 5px; + cursor:default; +} +.feature_level_list p:first-child { + font-weight:bold; + font-size:16px; +} +.feature_level_list p:first-child em { + font-weight:normal; + font-size:14px; + padding-left:5px; +} +#map .leaflet-overlay-pane .c3nav-highlight { + pointer-events:none; +} +#btn_editing_cancel { + margin-right:8px; +} diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index 17f0fcab..8ad8edbf 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -1,6 +1,7 @@ editor = { feature_types: {}, feature_types_order: [], + _highlight_layer: null, init: function () { // Init Map @@ -14,6 +15,7 @@ editor = { }); L.control.scale({imperial: false}).addTo(editor.map); + editor._highlight_layer = L.layerGroup().addTo(editor.map); editor.get_feature_types(); editor.get_packages(); @@ -29,7 +31,7 @@ editor = { editor.feature_types[feature_type.name] = feature_type; editor.feature_types_order.push(feature_type.name); editcontrols.append( - $('
').attr('name', feature_type.name).append( + $('
').attr('name', feature_type.name).append( $('').text(feature_type.title_plural).append( $('') ) @@ -101,17 +103,21 @@ editor = { editor.set_current_level($(this).attr('name')); }); - var level; + var level, feature_type; for (var i = 0; i < levels.length; i++) { level = levels[i]; editor.levels[level.name] = level; editor.level_feature_layers[level.name] = {}; for (var j = 0; j < editor.feature_types_order.length; j++) { - editor.level_feature_layers[level.name][editor.feature_types_order[j]] = L.layerGroup(); + feature_type = editor.feature_types_order[j]; + editor.level_feature_layers[level.name][feature_type] = L.layerGroup(); + $('.feature_list[name='+feature_type+']').append( + $('
    ').attr('data-level', level.name) + ); } } editor.set_current_level(levels[levels.length - 1].name); - editor.init_drawing(); + editor.init_features(); }); }, set_current_level: function(level_name) { @@ -125,11 +131,13 @@ editor = { editor._level = level_name; $('.leaflet-levels .current').removeClass('current'); $('.leaflet-levels a[name='+level_name+']').addClass('current'); + $('.feature_level_list').hide(); + $('.feature_level_list[data-level='+level_name+']').show(); }, _creating: null, _editing: null, - init_drawing: function () { + init_features: function () { // Add drawing new features L.DrawControl = L.Control.extend({ options: { @@ -150,29 +158,38 @@ editor = { }); editor.map.addControl(new L.DrawControl()); - $('#mapeditlist').on('click', '.start-drawing', function () { - editor.start_creating($(this).closest('fieldset').attr('name')); - }); + $('#mapeditlist').on('click', '.start-drawing', editor._click_start_drawing) + .on('mouseenter', '.feature_level_list li', editor._hover_feature_detail) + .on('mouseleave', '.feature_level_list li', editor._unhover_feature_detail) + .on('click', '.feature_level_list li', editor._click_feature_detail); editor.map.on('editable:drawing:commit', editor.done_creating); editor.map.on('editable:editing', editor.update_editing); editor.map.on('editable:drawing:cancel', editor._canceled_creating); $('#mapeditdetail').on('click', '#btn_editing_cancel', editor.cancel_editing) - .on('submit', 'form', editor.submit_editing); + .on('click', 'button[type=submit]', editor.submit_editing_btn_click) + .on('submit', 'form', editor.submit_editing) + editor.get_features(); }, + features: {}, get_features: function () { $.getJSON('/api/v1/features/', function(features) { - for (level in editor.level_layers) { - editor.level_layers[level].clearLayers(); + var feature_type; + for (var level in editor.levels) { + for (var j = 0; j < editor.feature_types_order.length; j++) { + feature_type = editor.feature_types_order[j]; + editor.level_feature_layers[level][feature_type].clearLayers(); + } } - var feature; + $('.feature_level_list li').remove(); + var feature, layergroup; for (var i=0; i < features.length; i++) { feature = features[i]; - L.geoJSON({ + layergroup = L.geoJSON({ type: 'Feature', geometry: feature.geometry, properties: { @@ -181,20 +198,86 @@ editor = { } }, { style: editor._get_feature_style - }).addTo(editor.level_feature_layers[feature.level][feature.feature_type]); + }).on('mouseover', editor._hover_feature_layer) + .on('mouseout', editor._unhover_feature_layer) + .on('click', editor._click_feature_layer) + .addTo(editor.level_feature_layers[feature.level][feature.feature_type]); + feature.layer = layergroup.getLayers()[0]; + editor.features[feature.name] = feature; + + $('.feature_list[name='+feature.feature_type+'] > [data-level='+feature.level+']').append( + $('
  • ').attr('name', feature.name).append( + $('

    ').text(feature.title).append(' ').append( + $('').text(feature.name) + ) + ) + ); } $('.start-drawing').show(); $('#mapeditcontrols').addClass('list'); editor.set_current_level(editor._level); }); - }, _get_feature_style: function (feature) { return editor.feature_types[feature.properties.feature_type]; }, + _click_start_drawing: function (e) { + editor.start_creating($(this).closest('fieldset').attr('name')); + }, + _hover_feature_detail: function (e) { + editor._highlight_layer.clearLayers(); + L.geoJSON(editor.features[$(this).attr('name')].geometry, { + style: function() { + return { + color: '#FFFFEE', + opacity: 0.5, + fillOpacity: 0.5, + className: 'c3nav-highlight' + }; + } + }).addTo(editor._highlight_layer); + }, + _unhover_feature_detail: function () { + editor._highlight_layer.clearLayers(); + }, + _click_feature_detail: function() { + editor.start_editing($(this).attr('name')); + }, + + _hover_feature_layer: function (e) { + editor._unhover_feature_layer(); + if (editor._editing === null && editor._creating === null) { + editor._highlight_layer.clearLayers(); + L.geoJSON(e.layer.toGeoJSON(), { + style: function() { + return { + color: '#FFFFEE', + opacity: 0.5, + fillOpacity: 0.5, + className: 'c3nav-highlight' + }; + } + }).addTo(editor._highlight_layer); + } + $('.feature_list li[name='+e.layer.feature.properties.name+']').addClass('hover'); + }, + _unhover_feature_layer: function (e) { + editor._highlight_layer.clearLayers(); + $('.feature_list .hover').removeClass('hover'); + }, + _click_feature_layer: function(e) { + editor.start_editing(e.layer.feature.properties.name); + if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey) && this.editEnabled()) { + if (e.layer.feature.properties.geomtype == 'polygon') { + this.editor.newHole(e.latlng); + } + } + }, + start_creating: function (feature_type) { if (editor._creating !== null || editor._editing !== null) return; + editor._highlight_layer.clearLayers(); editor._creating = feature_type; var options = editor.feature_types[feature_type]; if (options.geomtype == 'polygon') { @@ -230,8 +313,13 @@ editor = { } }, - start_editing: function () { - // todo + start_editing: function (name) { + if (editor._creating !== null || editor._editing !== null) return; + editor._highlight_layer.clearLayers(); + editor._editing = editor.features[name].layer; + var path = '/editor/features/edit/' + name + '/'; + $('#mapeditcontrols').removeClass('list'); + $('#mapeditdetail').load(path, editor.edit_form_loaded); }, edit_form_loaded: function() { $('#mapeditcontrols').addClass('detail'); @@ -263,9 +351,18 @@ editor = { editor.get_features(); } }, - submit_editing: function(e) { + submit_editing_btn_click: function(e) { + e.preventDefault(); + $(this).closest('form').trigger('submit', $(this)); + }, + submit_editing: function(e, btn) { e.preventDefault(); var data = $(this).serialize(); + console.log($(btn)); + console.log($(btn).is('[name]')); + if (btn !== undefined && $(btn).is('[name]')) { + data += '&'+$('').attr('name', $(btn).attr('name')).val($(btn).val()).serialize(); + } var action = $(this).attr('action'); $('#mapeditcontrols').removeClass('detail'); $('#mapeditdetail').html(''); diff --git a/src/c3nav/editor/templates/editor/feature.html b/src/c3nav/editor/templates/editor/feature.html index 75fca294..0ed4cafb 100644 --- a/src/c3nav/editor/templates/editor/feature.html +++ b/src/c3nav/editor/templates/editor/feature.html @@ -5,11 +5,16 @@ {% csrf_token %} {% bootstrap_form form %} {% buttons %} - + {% if not new %} + + {% endif %} + {% endbuttons %} diff --git a/src/c3nav/editor/templates/editor/feature_delete.html b/src/c3nav/editor/templates/editor/feature_delete.html new file mode 100644 index 00000000..9eb8343d --- /dev/null +++ b/src/c3nav/editor/templates/editor/feature_delete.html @@ -0,0 +1,17 @@ +{% load bootstrap3 %} + +

    Delete {{ feature_type.title }}

    +
    + {% csrf_token %} +

    Please confirm deleting: {{ name }}

    + + + {% buttons %} + + + {% endbuttons %} +
    diff --git a/src/c3nav/editor/urls.py b/src/c3nav/editor/urls.py index 51ce70a4..7b80e5df 100644 --- a/src/c3nav/editor/urls.py +++ b/src/c3nav/editor/urls.py @@ -1,9 +1,10 @@ from django.conf.urls import url from django.views.generic import TemplateView -from c3nav.editor.views import add_feature +from c3nav.editor.views import add_feature, edit_feature urlpatterns = [ url(r'^$', TemplateView.as_view(template_name='editor/map.html'), name='editor.index'), - url(r'^features/(?P[^/]+)/add/$', add_feature, name='editor.feature.add') + url(r'^features/(?P[^/]+)/add/$', add_feature, name='editor.feature.add'), + url(r'^features/edit/(?P[^/]+)/$', edit_feature, name='editor.feature.edit') ] diff --git a/src/c3nav/editor/views.py b/src/c3nav/editor/views.py index f7bd64af..c4031940 100644 --- a/src/c3nav/editor/views.py +++ b/src/c3nav/editor/views.py @@ -1,10 +1,12 @@ from django.conf import settings +from django.core.exceptions import PermissionDenied from django.db import transaction from django.http.response import Http404 -from django.shortcuts import render +from django.shortcuts import get_object_or_404, render from c3nav.editor.forms import FeatureForm -from c3nav.mapdata.models.feature import FEATURE_TYPES +from c3nav.mapdata.models.feature import FEATURE_TYPES, Feature +from c3nav.mapdata.permissions import can_access_package def add_feature(request, feature_type): @@ -37,3 +39,47 @@ def add_feature(request, feature_type): 'path': request.path, 'new': True }) + + +def edit_feature(request, name): + feature = get_object_or_404(Feature, name=name) + if not can_access_package(feature.package): + raise PermissionDenied + feature_type = FEATURE_TYPES.get(feature.feature_type) + + if request.method == 'POST': + if request.POST.get('delete') == '1': + if request.POST.get('delete_confirm') == '1': + feature.delete() + return render(request, 'editor/feature_success.html', {}) + + return render(request, 'editor/feature_delete.html', { + 'name': feature.name, + 'feature_type': feature_type, + 'path': request.path + }) + + form = FeatureForm(instance=feature, data=request.POST, feature_type=feature_type) + if form.is_valid(): + if not settings.DIRECT_EDITING: + return render(request, 'editor/feature_success.html', {}) + + with transaction.atomic(): + feature = form.instance + feature.feature_type = feature_type.name + feature.titles = {} + for language, title in form.titles.items(): + if title: + feature.titles[language] = title + feature.save() + + return render(request, 'editor/feature_success.html', {}) + else: + form = FeatureForm(instance=feature, feature_type=feature_type) + + return render(request, 'editor/feature.html', { + 'form': form, + 'feature_type': feature_type, + 'path': request.path, + 'new': False + }) diff --git a/src/c3nav/mapdata/api.py b/src/c3nav/mapdata/api.py index 0bec0253..b6cc486e 100644 --- a/src/c3nav/mapdata/api.py +++ b/src/c3nav/mapdata/api.py @@ -4,7 +4,6 @@ import os from django.conf import settings from django.core.files import File from django.http import Http404, HttpResponse - from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet diff --git a/src/c3nav/mapdata/models/feature.py b/src/c3nav/mapdata/models/feature.py index 77668afb..20ae3690 100644 --- a/src/c3nav/mapdata/models/feature.py +++ b/src/c3nav/mapdata/models/feature.py @@ -7,6 +7,7 @@ from django.utils.translation import get_language from shapely.geometry import mapping, shape from c3nav.mapdata.utils import sort_geojson + from ..fields import GeometryField, JSONField @@ -35,7 +36,7 @@ class Feature(models.Model): """ TYPES = tuple((name, t.title) for name, t in FEATURE_TYPES.items()) - name = models.SlugField(_('feature identifier'), primary_key=True, max_length=50, help_text=_('e.g. noc')) + name = models.SlugField(_('feature identifier'), primary_key=True, max_length=50) package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='features', verbose_name=_('map package')) feature_type = models.CharField(max_length=50, choices=TYPES) diff --git a/src/c3nav/mapdata/packageio/const.py b/src/c3nav/mapdata/packageio/const.py index 05c6ad4f..098ffbd0 100644 --- a/src/c3nav/mapdata/packageio/const.py +++ b/src/c3nav/mapdata/packageio/const.py @@ -1,4 +1,3 @@ from ..models import Feature, Level, Package, Source - ordered_models = (Package, Level, Source, Feature) diff --git a/src/c3nav/mapdata/permissions.py b/src/c3nav/mapdata/permissions.py index 651ea9b4..2fa1c6c0 100644 --- a/src/c3nav/mapdata/permissions.py +++ b/src/c3nav/mapdata/permissions.py @@ -1,6 +1,5 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ - from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import BasePermission