rebuild editor

This commit is contained in:
Laura Klünder 2016-11-27 23:51:44 +01:00
parent 769343f78a
commit 19da48c915
16 changed files with 457 additions and 414 deletions

View file

@ -13,7 +13,7 @@ from c3nav.mapdata.models.geometry import Area, Building, Door, Obstacle
from c3nav.mapdata.permissions import get_unlocked_packages from c3nav.mapdata.permissions import get_unlocked_packages
class FeatureFormMixin(ModelForm): class MapitemFormMixin(ModelForm):
def __init__(self, *args, request=None, **kwargs): def __init__(self, *args, request=None, **kwargs):
self.request = request self.request = request
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -73,13 +73,13 @@ class FeatureFormMixin(ModelForm):
self.titles = titles self.titles = titles
def create_editor_form(feature_model, add_fields=None): def create_editor_form(mapdata_model, add_fields=None):
class EditorForm(FeatureFormMixin, ModelForm): class EditorForm(MapitemFormMixin, ModelForm):
class Meta: class Meta:
model = feature_model model = mapdata_model
fields = ['name', 'package', 'level', 'geometry'] + (add_fields if add_fields is not None else []) fields = ['name', 'package', 'level', 'geometry'] + (add_fields if add_fields is not None else [])
feature_model.EditorForm = EditorForm mapdata_model.EditorForm = EditorForm
def create_editor_forms(): def create_editor_forms():

View file

@ -33,43 +33,36 @@ body {
position: absolute; position: absolute;
top: 54px; top: 54px;
bottom: 0; bottom: 0;
padding:0;
width: 350px; width: 350px;
right: 0; right: 0;
overflow:hidden; overflow: auto;
padding:8px;
}
#mapeditcontrols.loading {
background-image:url('/static/img/loader.gif'); background-image:url('/static/img/loader.gif');
background-repeat:no-repeat; background-repeat:no-repeat;
background-position:center; background-position:center;
} }
#mapeditcontrols > div { #mapeditcontrols h3 {
position:absolute;
top:0;
bottom:0;
background-color:white;
padding:8px;
width:350px;
right:0;
transition: right 300ms;
-webkit-transition: right 300ms;
overflow:auto;
}
#mapeditcontrols > #mapeditdetail {
right:-350px;
pointer-events: none;
}
#mapeditcontrols.detail #mapeditdetail {
right:0;
pointer-events: auto;
}
#mapeditcontrols > #mapeditlist {
display:none;
}
#mapeditcontrols.list > #mapeditlist {
display:block;
}
#mapeditdetail h3 {
margin-top:5px; margin-top:5px;
} }
a.list-group-item, a.list-group-item:hover {
color:#158cba;
}
a.list-group-item .badge {
font-size:14px;
font-weight:bold;
}
.itemtable td:last-child {
text-align:right;
}
.itemtable tr.highlight td {
background-color:#FFFFDD;
}
.cancel-btn {
margin-right:8px;
}
#mapeditlist legend .btn { #mapeditlist legend .btn {
padding-left:5.5px; padding-left:5.5px;
padding-right:5.5px; padding-right:5.5px;
@ -89,36 +82,6 @@ legend {
.leaflet-editable-drawing .leaflet-overlay-pane .leaflet-interactive { .leaflet-editable-drawing .leaflet-overlay-pane .leaflet-interactive {
cursor:crosshair; cursor:crosshair;
} }
.feature_list {
display:none;
}
.feature_level_list {
list-style-type:none;
display:none;
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 { #map .leaflet-overlay-pane .c3nav-highlight {
pointer-events:none; pointer-events:none;
} }

View file

@ -13,9 +13,11 @@ editor = {
editable: true, editable: true,
closePopupOnClick: false closePopupOnClick: false
}); });
editor.map.on('click', function (e) {
editor.map.doubleClickZoom.enable();
});
L.control.scale({imperial: false}).addTo(editor.map); L.control.scale({imperial: false}).addTo(editor.map);
editor._highlight_layer = L.layerGroup().addTo(editor.map);
$('#show_map').click(function() { $('#show_map').click(function() {
$('body').removeClass('controls'); $('body').removeClass('controls');
@ -24,50 +26,17 @@ editor = {
$('body').addClass('controls'); $('body').addClass('controls');
}); });
editor.get_feature_types(); editor.init_geometries();
editor.init_sidebar();
editor.get_packages(); editor.get_packages();
editor.get_sources(); editor.get_sources();
},
_feature_type: null,
get_feature_types: function () {
$.getJSON('/api/featuretypes/', function (feature_types) {
var feature_type;
var listcontainer = $('#mapeditlist fieldset');
var dropdown = $('#featuretype_dropdown').on('click', 'a', function(e) {
e.preventDefault();
editor.set_current_feature_type($(this).parent().attr('data-name'));
});
for (var i = 0; i < feature_types.length; i++) {
feature_type = feature_types[i];
editor.feature_types[feature_type.name] = feature_type;
feature_type.weight = 0;
feature_type.fillOpacity = 0.6;
feature_type.smoothFactor = 0;
editor.feature_types_order.push(feature_type.name);
listcontainer.append(
$('<div class="feature_list">').attr('name', feature_type.name)
);
dropdown.append(
$('<li>').attr('data-name', feature_type.name).append(
$('<a href="#">').text(feature_type.title_plural)
)
);
}
editor.set_current_feature_type(editor.feature_types_order[0]);
editor.get_levels(); editor.get_levels();
});
},
set_current_feature_type: function(feature_type) {
editor._feature_type = feature_type;
$('.feature_list').hide();
$('.feature_list[name='+feature_type+']').show();
$('#current_featuretype_title').text(editor.feature_types[feature_type].title_plural);
$('#create_featuretype_title').text(editor.feature_types[feature_type].title);
}, },
// packages
packages: {}, packages: {},
get_packages: function () { get_packages: function () {
// load packages
$.getJSON('/api/packages/', function (packages) { $.getJSON('/api/packages/', function (packages) {
var bounds = [[0, 0], [0, 0]]; var bounds = [[0, 0], [0, 0]];
var pkg; var pkg;
@ -83,8 +52,10 @@ editor = {
}); });
}, },
// sources
sources: {}, sources: {},
get_sources: function () { get_sources: function () {
// load sources
$.getJSON('/api/sources/', function (sources) { $.getJSON('/api/sources/', function (sources) {
var layers = {}; var layers = {};
var source; var source;
@ -98,10 +69,11 @@ editor = {
}); });
}, },
// levels
levels: {}, levels: {},
_level: null, _level: null,
level_feature_layers: {},
get_levels: function () { get_levels: function () {
// load levels and set the lowest one afterwards
$.getJSON('/api/levels/?ordering=-altitude', function (levels) { $.getJSON('/api/levels/?ordering=-altitude', function (levels) {
L.LevelControl = L.Control.extend({ L.LevelControl = L.Control.extend({
options: { options: {
@ -127,284 +99,299 @@ editor = {
editor.set_current_level($(this).attr('name')); editor.set_current_level($(this).attr('name'));
}); });
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++) {
feature_type = editor.feature_types_order[j];
editor.level_feature_layers[level.name][feature_type] = L.layerGroup();
$('.feature_list[name='+feature_type+']').append(
$('<ul class="feature_level_list">').attr('data-level', level.name)
);
}
}
editor.set_current_level(levels[levels.length - 1].name); editor.set_current_level(levels[levels.length - 1].name);
editor.init_features();
}); });
}, },
set_current_level: function(level_name) { set_current_level: function(level_name) {
if (editor._creating !== null || editor._editing !== null) return; // sets the current level if the sidebar allows it
for (var i = 0; i < editor.feature_types_order.length; i++) { var level_switch = $('#mapeditcontrols ').find('[data-level-switch]');
if (editor._level !== null) { if (level_switch.length === 0) return;
editor.level_feature_layers[editor._level][editor.feature_types_order[i]].remove();
}
editor.level_feature_layers[level_name][editor.feature_types_order[i]].addTo(editor.map);
}
editor._level = level_name; editor._level = level_name;
$('.leaflet-levels .current').removeClass('current'); $('.leaflet-levels .current').removeClass('current');
$('.leaflet-levels a[name='+level_name+']').addClass('current'); $('.leaflet-levels a[name='+level_name+']').addClass('current');
$('.feature_level_list').hide(); editor.get_geometries();
$('.feature_level_list[data-level='+level_name+']').show();
},
_creating: null, var level_switch_href = level_switch.attr('data-level-switch');
_editing: null, if (level_switch_href) {
init_features: function () { editor.sidebar_get(level_switch_href.replace('LEVEL', level_name));
$('#start-drawing').click(editor._click_start_drawing);
$('#cancel-drawing').click(editor.cancel_creating);
$('#mapeditlist').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('click', 'button[type=submit]', editor.submit_editing_btn_click)
.on('submit', 'form', editor.submit_editing)
editor.get_features();
},
features: {},
get_features: function () {
$.getJSON('/api/features/?ordering=name', function(all_features) {
$('.feature_level_list li').remove();
var feature_type, features, feature, layergroup;
for (var j = 0; j < editor.feature_types_order.length; j++) {
feature_type = editor.feature_types_order[j];
for (var level in editor.levels) {
editor.level_feature_layers[level][feature_type].clearLayers();
} }
features = all_features[editor.feature_types[feature_type].endpoint] },
for (var i = 0; i < features.length; i++) { // geometries
feature = features[i]; _geometries_layer: null,
layergroup = L.geoJSON({ _highlight_layer: null,
_editing_layer: null,
_get_geometries_next_time: false,
_geometries: {},
_creating: false,
_editing: null,
init_geometries: function () {
// init geometries and edit listeners
editor._highlight_layer = L.layerGroup().addTo(editor.map);
editor._editing_layer = L.layerGroup().addTo(editor.map);
$('#mapeditcontrols').on('mouseenter', '.itemtable tr[name]', editor._hover_mapitem_row)
.on('mouseleave', '.itemtable tr[name]', editor._unhighlight_geometry);
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);
},
get_geometries: function () {
// reload geometries of current level
editor._geometries = {};
if (editor._geometries_layer !== null) {
editor.map.removeLayer(editor._geometries_layer);
}
$.getJSON('/api/geometries/?level='+String(editor._level), function(geometries) {
editor._geometries_layer = L.geoJSON(geometries, {
style: editor._get_geometry_style,
onEachFeature: editor._register_geojson_feature,
});
editor._geometries_layer.addTo(editor.map);
});
},
_geometry_colors: {
'building': '#333333',
'area': '#FFFFFF',
'obstacle': '#999999',
'door': '#FF00FF',
},
_get_geometry_style: function (feature) {
// style callback for GeoJSON loader
return editor._get_mapitem_type_style(feature.properties.type);
},
_get_mapitem_type_style: function (mapitem_type) {
// get styles for a specific mapitem
return {
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;
layer.on('mouseover', editor._hover_geometry_layer)
.on('mouseout', editor._unhighlight_geometry)
.on('click', editor._click_geometry_layer)
.on('dblclick', editor._dblclick_geometry_layer)
},
// hover and highlight geometries
_hover_mapitem_row: function (e) {
// hover callback for a itemtable row
editor._highlight_geometry($(this).closest('.itemtable').attr('data-mapitem-type'), $(this).attr('name'));
},
_hover_geometry_layer: function (e) {
// hover callback for a geometry layer
editor._highlight_geometry(e.target.feature.properties.type, e.target.feature.properties.name);
},
_click_geometry_layer: function (e) {
// click callback for a geometry layer scroll the corresponding itemtable row into view if it exists
var properties = e.target.feature.properties;
var row = $('.itemtable[data-mapitem-type='+properties.type+'] tr[name='+properties.name+']');
if (row.length) {
row[0].scrollIntoView();
}
},
_dblclick_geometry_layer: function (e) {
// dblclick callback for a geometry layer - edit this feature if the corresponding itemtable row exists
var properties = e.target.feature.properties;
var row = $('.itemtable[data-mapitem-type='+properties.type+'] tr[name='+properties.name+']');
if (row.length) {
row.find('td:last-child a').click();
editor.map.doubleClickZoom.disable();
}
},
_highlight_geometry: function(mapitem_type, name) {
// highlight a geometries layer and itemtable row if they both exist
var pk = mapitem_type+'-'+name;
editor._unhighlight_geometry();
var layer = editor._geometries[pk];
var row = $('.itemtable[data-mapitem-type='+mapitem_type+'] tr[name='+name+']');
if (layer !== undefined && row.length) {
row.addClass('highlight');
L.geoJSON(layer.feature, {
style: function() {
return {
color: '#FFFFDD',
weight: 3,
opacity: 0.7,
fillOpacity: 0,
className: 'c3nav-highlight'
};
}
}).addTo(editor._highlight_layer);
}
},
_unhighlight_geometry: function() {
// unhighlight whatever is highlighted currently
editor._highlight_layer.clearLayers();
$('.itemtable .highlight').removeClass('highlight');
},
// edit and create geometries
_check_start_editing: function() {
// called on sidebar load. start editing or creating depending on how the sidebar may require it
var geometry_field = $('#mapeditcontrols').find('input[name=geometry]');
if (geometry_field.length) {
var form = geometry_field.closest('form');
var mapitem_type = form.attr('data-mapitem-type');
if (form.is('[data-name]')) {
// edit existing geometry
var name = form.attr('data-name');
var pk = mapitem_type+'-'+name;
editor._geometries_layer.removeLayer(editor._geometries[pk]);
editor._editing = L.geoJSON({
type: 'Feature', type: 'Feature',
geometry: feature.geometry, geometry: JSON.parse(geometry_field.val()),
properties: { properties: {
name: feature.name, type: mapitem_type,
feature_type: feature_type
} }
}, { }, {
style: editor._get_feature_style style: editor._get_geometry_style
}).on('mouseover', editor._hover_feature_layer) }).getLayers()[0];
.on('mouseout', editor._unhover_feature_layer) editor._editing.on('click', editor._click_editing_layer);
.on('click', editor._click_feature_layer) editor._editing.addTo(editor._editing_layer);
.addTo(editor.level_feature_layers[feature.level][feature_type]); editor._editing.enableEdit();
feature.layer = layergroup.getLayers()[0]; } else if (form.is('[data-geomtype]')) {
editor.features[feature.name] = feature; // create new geometry
var geomtype = form.attr('data-geomtype');
$('.feature_list[name=' + feature_type + '] > [data-level=' + feature.level + ']').append( var options = editor._get_mapitem_type_style(mapitem_type);
$('<li>').attr('name', feature.name).append( if (geomtype == 'polygon') {
$('<p>').text(feature.title).append(' ').append(
$('<em>').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(editor._feature_type);
},
_hover_feature_detail: function (e) {
editor._highlight_layer.clearLayers();
L.geoJSON(editor.features[$(this).attr('name')].geometry, {
style: function() {
return {
color: '#FFFFDD',
weight: 3,
opacity: 0.7,
fillOpacity: 0,
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 && e.layer.feature.properties.feature_type == editor._feature_type) {
editor._highlight_layer.clearLayers();
L.geoJSON(e.layer.toGeoJSON(), {
style: function() {
return {
color: '#FFFFDD',
weight: 3,
opacity: 0.7,
fillOpacity: 0,
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) {
if (e.layer.feature.properties.feature_type != editor._feature_type) return;
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') {
editor.map.editTools.startPolygon(null, options); editor.map.editTools.startPolygon(null, options);
} else if (options.geomtype == 'polyline') { } else if (geomtype == 'polyline') {
editor.map.editTools.startPolyline(null, options); editor.map.editTools.startPolyline(null, options);
} }
$('#cancel-drawing').show(); editor._creating = true;
$('#start-drawing').hide(); $('#id_level').val(editor._level);
$('body').removeClass('controls'); }
} else if (editor._get_geometries_next_time) {
editor.get_geometries();
editor._get_geometries_next_time = false;
}
}, },
cancel_creating: function () { _cancel_editing: function() {
if (editor._creating === null || editor._editing !== null) return; // called on sidebar unload. cancel all editing and creating.
if (editor._editing !== null) {
editor._editing_layer.clearLayers();
editor._editing.disableEdit();
editor._editing = null;
editor._get_geometries_next_time = true;
}
if (editor._creating) {
editor._creating = false;
editor.map.editTools.stopDrawing(); editor.map.editTools.stopDrawing();
editor._creating = null; }
$('#cancel-drawing').hide();
}, },
_canceled_creating: function (e) { _canceled_creating: function (e) {
if (editor._creating !== null && editor._editing === null) { // called after we canceled creating so we can remove the temporary layer.
if (!editor._creating) {
e.layer.remove(); e.layer.remove();
$('#start-drawing').show();
} }
}, },
done_creating: function(e) { _click_editing_layer: function(e) {
if (editor._creating !== null && editor._editing === null) { // click callback for a currently edited layer. create a hole on ctrl+click.
editor._editing = e.layer; if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey)) {
editor._editing.disableEdit(); if (e.target.feature.geometry.type == 'Polygon') {
editor.map.fitBounds(editor._editing.getBounds());
$('#cancel-drawing').hide();
var path = '/editor/features/' + editor._creating + '/add/';
$('#mapeditcontrols').removeClass('list');
$('body').addClass('controls');
$('#mapeditdetail').load(path, editor.edit_form_loaded);
}
},
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/'+ editor._editing.feature.properties.feature_type +'/edit/' + name + '/';
$('#mapeditcontrols').removeClass('list');
$('#mapeditdetail').load(path, editor.edit_form_loaded);
$('body').addClass('controls');
},
edit_form_loaded: function() {
$('#mapeditcontrols').addClass('detail');
$('#id_level').val(editor._level);
if ($('#id_geometry').length) {
$('#id_geometry').val(JSON.stringify(editor._editing.toGeoJSON().geometry));
editor._editing.enableEdit();
}
if (editor._editing.options.geomtype == 'polygon') {
editor._editing.on('click', function (e) {
if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey) && this.editEnabled()) {
this.editor.newHole(e.latlng); this.editor.newHole(e.latlng);
} }
});
} }
}, },
update_editing: function () { _done_creating: function(e) {
// called when creating is completed (by clicking on the last point). fills in the form and switches to editing.
if (editor._creating) {
editor._creating = false;
editor._editing = e.layer;
editor._editing.addTo(editor._editing_layer);
editor._editin.on('click', editor._click_editing_layer);
editor._update_editing();
}
},
_update_editing: function () {
// called if the temporary drawing layer changes. if we are in editing mode (not creating), update the form.
if (editor._editing !== null) { if (editor._editing !== null) {
$('#id_geometry').val(JSON.stringify(editor._editing.toGeoJSON().geometry)); $('#id_geometry').val(JSON.stringify(editor._editing.toGeoJSON().geometry));
} }
}, },
cancel_editing: function() {
if (editor._editing !== null) { // sidebar
if (editor._creating !== null) { sidebar_location: null,
editor._editing.remove(); init_sidebar: function() {
} // init the sidebar. sed listeners for form submits and link clicks
editor._editing = null; $('#mapeditcontrols').on('click', 'a[href]', editor._sidebar_link_click)
editor._creating = null; .on('click', 'button[type=submit]', editor._sidebar_submit_btn_click)
$('#mapeditcontrols').removeClass('detail'); .on('submit', 'form', editor._sidebar_submit);;
$('#mapeditdetail').html('');
editor.get_features(); editor.sidebar_get('mapitemtypes/'+String(editor._level)+'/');
}
}, },
submit_editing_btn_click: function(e) { sidebar_get: function(location) {
// load a new page into the sidebar using a GET request
editor._sidebar_unload();
$.get(location, editor._sidebar_loaded);
},
_sidebar_unload: function(location) {
// unload the sidebar. called on sidebar_get and form submit.
$('#mapeditcontrols').html('').addClass('loading');
editor._unhighlight_geometry();
editor._cancel_editing();
},
_sidebar_loaded: function(data) {
// sidebar was loaded. load the content. check if there are any redirects. call _check_start_editing.
var content = $(data);
var mapeditcontrols = $('#mapeditcontrols');
mapeditcontrols.html(content).removeClass('loading');
var redirect = mapeditcontrols.find('form[name=redirect]');
if (redirect.length) {
redirect.submit();
return;
}
redirect = $('span[data-redirect]');
if (redirect.length) {
editor.sidebar_get(redirect.attr('data-redirect').replace('LEVEL', editor._level));
return;
}
editor._check_start_editing();
},
_sidebar_link_click: function(e) {
// listener for link-clicks in the sidebar.
e.preventDefault();
var href = $(this).attr('href');
if ($(this).is('[data-insert-level]')) {
href = href.replace('LEVEL', editor._level);
}
editor.sidebar_get(href);
},
_sidebar_submit_btn_click: function(e) {
// listener for submit-button-clicks in the sidebar, so the submit event will know which button submitted.
$(this).closest('form').data('btn', $(this)).clearQueue().delay(300).queue(function() { $(this).closest('form').data('btn', $(this)).clearQueue().delay(300).queue(function() {
$(this).data('button', null); $(this).data('button', null);
}); });
}, },
submit_editing: function(e) { _sidebar_submit: function(e) {
// listener for form submits in the sidebar.
if ($(this).attr('name') == 'redirect') return; if ($(this).attr('name') == 'redirect') return;
e.preventDefault(); e.preventDefault();
editor._sidebar_unload();
var data = $(this).serialize(); var data = $(this).serialize();
var btn = $(this).data('btn'); var btn = $(this).data('btn');
if (btn !== undefined && btn !== null && $(btn).is('[name]')) { if (btn !== undefined && btn !== null && $(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();
} }
var action = $(this).attr('action'); var action = $(this).attr('action');
$('#mapeditcontrols').removeClass('detail'); $.post(action, data, editor._sidebar_loaded);
$('#mapeditdetail').html('');
editor._editing.disableEdit();
$.post(action, data, function (data) {
var content = $(data);
if ($('<div>').append(content).find('form').length > 0) {
$('#mapeditcontrols').addClass('detail');
$('#mapeditdetail').html(content).find('form[name=redirect]').submit();
if ($('#id_geometry').length) {
editor._editing.enableEdit();
}
} else {
if (editor._creating !== null) {
editor._editing.remove();
}
editor._editing = null;
editor._creating = null;
editor.get_features();
$('body').removeClass('controls');
}
});
} }
}; };
if ($('#mapeditlist').length) { if ($('#mapeditcontrols').length) {
editor.init(); editor.init();
} }

View file

@ -1,20 +0,0 @@
{% load bootstrap3 %}
<h3>{% if new %}New{% else %}Edit{% endif %} {{ feature_type.title }}</h3>
<form action="{{ path }}" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
{% if not new %}
<button type="submit" name="delete" value="1" class="btn btn-danger">
Delete
</button>
{% endif %}
<button type="submit" class="btn btn-primary pull-right">
Save
</button>
<button type="button" id="btn_editing_cancel" class="btn {% if new %}btn-danger{% else %}btn-default pull-right{% endif %}">
Cancel
</button>
{% endbuttons %}
</form>

View file

@ -12,22 +12,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="map"></div> <div id="map"></div>
<div id="mapeditcontrols"> <div id="mapeditcontrols" class="loading">
<div id="mapeditlist"> <span data-level-switch="{% url 'editor.mapitemtypes' level='LEVEL' %}"></span>
<fieldset>
<legend>
<div class="dropdown">
<span id="featuretype_dropdown_btn" class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span id="current_featuretype_title"></span>
<span class="caret"></span>
</span>
<ul class="dropdown-menu" id="featuretype_dropdown" aria-labelledby="featuretype_dropdown_btn"></ul>
</div>
<button class="btn btn-default btn-xs pull-right" id="start-drawing">create new <span id="create_featuretype_title"></span></button>
<button class="btn btn-danger btn-xs pull-right" id="cancel-drawing">cancel creating</button>
</legend>
</fieldset>
</div>
<div id="mapeditdetail"></div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,21 @@
{% load bootstrap3 %}
<h3>{% if new %}New{% else %}Edit{% endif %} {{ mapitem_type.title }}</h3>
<form action="{{ path }}" method="post" data-mapitem-type="{{ mapitem_type }}"{% if not new %} data-name="{{ name }}"{% elif geomtype %} data-geomtype="{{ geomtype }}"{% endif %}>
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
{% if not new %}
<button type="submit" name="delete" value="1" class="btn btn-danger">
Delete
</button>
{% endif %}
<button type="submit" class="btn btn-primary pull-right">
Save
</button>
<a class="btn {% if new %}btn-danger{% else %}btn-default pull-right{% endif %} cancel-btn"
href="{% url 'editor.mapitems.level' mapitem_type=mapitem_type level='LEVEL' %}" data-insert-level>
Cancel
</a>
{% endbuttons %}
</form>

View file

@ -1,15 +1,15 @@
{% load bootstrap3 %} {% load bootstrap3 %}
<h3>Delete {{ feature_type.title }}</h3> <h3>Delete {{ mapitem_type.title }}</h3>
<form action="{{ path }}" method="post"> <form action="{{ path }}" method="post">
{% csrf_token %} {% csrf_token %}
<p>Please confirm deleting: {{ name }}</p> <p>Please confirm deleting: {{ name }}</p>
<input type="hidden" name="delete" value="1"> <input type="hidden" name="delete" value="1">
<input type="hidden" name="name" value="{{ name }}"> <input type="hidden" name="name" value="{{ name }}">
{% buttons %} {% buttons %}
<button type="button" id="btn_editing_cancel" class="btn btn-default"> <a class="btn btn-default" href="{% url 'editor.mapitems' mapitem_type=mapitem_type level='LEVEL' %}" data-insert-level>
Cancel Cancel
</button> </a>
<button type="submit" name="delete_confirm" value="1" class="btn btn-danger pull-right"> <button type="submit" name="delete_confirm" value="1" class="btn btn-danger pull-right">
Delete Delete
</button> </button>

View file

@ -6,4 +6,6 @@
<img src="{% static 'img/loader.gif' %}"> <img src="{% static 'img/loader.gif' %}">
Redirecting… Redirecting…
</form> </form>
{% else %}
<span data-redirect="{% url 'editor.mapitems.level' mapitem_type=mapitem_type level='LEVEL' %}"></span>
{% endif %} {% endif %}

View file

@ -0,0 +1,22 @@
{% load bootstrap3 %}
<h3>{{ title }}{% if has_level %} <small>on level {{ level }}</small>{% endif %}</h3>
<p><a href="{% url 'editor.mapitemtypes' level='LEVEL' %}" data-insert-level>&laquo; Back</a><p>
<p><a href="{% url 'editor.mapitems.add' mapitem_type=mapitem_type %}">Add new</a><p>
<table class="table table-condensed itemtable" data-mapitem-type="{{ mapitem_type }}">
<tbody>
{% for item in items %}
<tr name="{{ item.name }}">
<td>{{ item.name }}</td>
<td><a href="{% url 'editor.mapitems.edit' mapitem_type=mapitem_type name=item.name %}">Edit</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% if has_level %}
<span data-level-switch="{% url 'editor.mapitems.level' mapitem_type=mapitem_type level='LEVEL' %}"></span>
{% else %}
<span data-level-switch></span>
{% endif %}

View file

@ -0,0 +1,19 @@
{% load bootstrap3 %}
<h3>Mapitem types</h3>
<div class="list-group">
{% for mapitemtype in mapitemtypes %}
{% if mapitemtype.has_level %}
{% url 'editor.mapitems.level' mapitem_type=mapitemtype.name level=level as list_url %}
{% else %}
{% url 'editor.mapitems' mapitem_type=mapitemtype.name as list_url %}
{% endif %}
<a href="{{ list_url }}" class="list-group-item">
{% if mapitemtype.has_level %}<span class="badge">{{ mapitemtype.count }}</span>{% endif %}
{{ mapitemtype.title }}
</a>
{% endfor %}
</div>
<span data-level-switch="{% url 'editor.mapitemtypes' level='LEVEL' %}"></span>

View file

@ -1,12 +1,15 @@
from django.conf.urls import url from django.conf.urls import url
from django.views.generic import TemplateView from django.views.generic import TemplateView
from c3nav.editor.views import edit_feature, finalize, oauth_callback from c3nav.editor.views import edit_mapitem, finalize, list_mapitems, list_mapitemtypes, oauth_callback
urlpatterns = [ urlpatterns = [
url(r'^$', TemplateView.as_view(template_name='editor/map.html'), name='editor.index'), url(r'^$', TemplateView.as_view(template_name='editor/map.html'), name='editor.index'),
url(r'^features/(?P<feature_type>[^/]+)/add/$', edit_feature, name='editor.feature.add'), url(r'^mapitemtypes/(?P<level>[^/]+)/$', list_mapitemtypes, name='editor.mapitemtypes'),
url(r'^features/(?P<feature_type>[^/]+)/edit/(?P<name>[^/]+)/$', edit_feature, name='editor.feature.edit'), url(r'^mapitems/(?P<mapitem_type>[^/]+)/list/$', list_mapitems, name='editor.mapitems'),
url(r'^mapitems/(?P<mapitem_type>[^/]+)/list/(?P<level>[^/]+)/$', list_mapitems, name='editor.mapitems.level'),
url(r'^mapitems/(?P<mapitem_type>[^/]+)/add/$', edit_mapitem, name='editor.mapitems.add'),
url(r'^mapitems/(?P<mapitem_type>[^/]+)/edit/(?P<name>[^/]+)/$', edit_mapitem, name='editor.mapitems.edit'),
url(r'^finalize/$', finalize, name='editor.finalize'), url(r'^finalize/$', finalize, name='editor.finalize'),
url(r'^oauth/(?P<hoster>[^/]+)/callback$', oauth_callback, name='editor.oauth.callback') url(r'^oauth/(?P<hoster>[^/]+)/callback$', oauth_callback, name='editor.oauth.callback')
] ]

View file

@ -3,95 +3,147 @@ from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.signing import BadSignature from django.core.signing import BadSignature
from django.http.response import Http404 from django.http.response import Http404
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils import translation from django.utils import translation
from c3nav.editor.hosters import get_hoster_for_package, hosters from c3nav.editor.hosters import get_hoster_for_package, hosters
from c3nav.mapdata.models import GEOMETRY_MAPITEM_TYPES from c3nav.mapdata.models.base import MAPITEM_TYPES
from c3nav.mapdata.models.package import Package from c3nav.mapdata.models.package import Package
from c3nav.mapdata.packageio.write import json_encode from c3nav.mapdata.packageio.write import json_encode
from c3nav.mapdata.permissions import can_access_package from c3nav.mapdata.permissions import can_access_package, filter_queryset_by_package_access
def edit_feature(request, feature_type, name=None): def list_mapitemtypes(request, level):
model = GEOMETRY_MAPITEM_TYPES.get(feature_type) def get_item_count(mapitemtype):
if model is None: if not hasattr(mapitemtype, 'level'):
return 0
return filter_queryset_by_package_access(request, mapitemtype.objects.filter(level__name=level)).count()
return render(request, 'editor/mapitemtypes.html', {
'level': level,
'mapitemtypes': [
{
'name': name,
'title': mapitemtype._meta.verbose_name_plural,
'has_level': hasattr(mapitemtype, 'level'),
'count': get_item_count(mapitemtype),
} for name, mapitemtype in MAPITEM_TYPES.items()
],
})
def list_mapitems(request, mapitem_type, level=None):
mapitemtype = MAPITEM_TYPES.get(mapitem_type)
if mapitemtype is None:
raise Http404('Unknown mapitemtype.')
if hasattr(mapitemtype, 'level') and level is None:
raise Http404('Missing level.')
elif not hasattr(mapitemtype, 'level') and level is not None:
raise redirect('editor.mapitems', mapitem_type=mapitem_type)
queryset = mapitemtype.objects.all()
if level is not None:
queryset = queryset.filter(level__name=level)
return render(request, 'editor/mapitems.html', {
'mapitem_type': mapitem_type,
'title': mapitemtype._meta.verbose_name_plural,
'has_level': level is not None,
'level': level,
'items': filter_queryset_by_package_access(request, queryset),
})
def edit_mapitem(request, mapitem_type, name=None):
mapitemtype = MAPITEM_TYPES.get(mapitem_type)
if mapitemtype is None:
raise Http404() raise Http404()
feature = None mapitem = None
if name is not None: if name is not None:
# Edit existing feature # Edit existing map item
feature = get_object_or_404(model, name=name) mapitem = get_object_or_404(mapitemtype, name=name)
if not can_access_package(request, feature.package): if not can_access_package(request, mapitem.package):
raise PermissionDenied raise PermissionDenied
new = mapitem is None
orig_name = mapitem.name if mapitem is not None else None
if request.method == 'POST': if request.method == 'POST':
if feature is not None and request.POST.get('delete') == '1': if mapitem is not None and request.POST.get('delete') == '1':
# Delete this feature! # Delete this mapitem!
if request.POST.get('delete_confirm') == '1': if request.POST.get('delete_confirm') == '1':
if not settings.DIRECT_EDITING: if not settings.DIRECT_EDITING:
with translation.override('en'): with translation.override('en'):
commit_msg = 'Deleted %s: %s' % (model._meta.verbose_name, feature.title) commit_msg = 'Deleted %s: %s' % (mapitemtype._meta.verbose_name, mapitem.title)
return render(request, 'editor/feature_success.html', { return render(request, 'editor/mapitem_success.html', {
'data': signing.dumps({ 'data': signing.dumps({
'type': 'editor.edit', 'type': 'editor.edit',
'action': 'delete', 'action': 'delete',
'package_name': feature.package.name, 'package_name': mapitem.package.name,
'commit_id': feature.package.commit_id, 'commit_id': mapitem.package.commit_id,
'commit_msg': commit_msg, 'commit_msg': commit_msg,
'file_path': feature.get_filename(), 'file_path': mapitem.get_filename(),
}) })
}) })
feature.delete() mapitem.delete()
return render(request, 'editor/feature_success.html', {}) return render(request, 'editor/mapitem_success.html', {
'mapitem_type': mapitem_type
})
return render(request, 'editor/feature_delete.html', { return render(request, 'editor/mapitem_delete.html', {
'name': feature.name, 'name': mapitem.name,
'feature_type': feature_type, 'mapitem_type': mapitem_type,
'path': request.path 'path': request.path
}) })
form = model.EditorForm(instance=feature, data=request.POST, request=request) form = mapitemtype.EditorForm(instance=mapitem, data=request.POST, request=request)
if form.is_valid(): if form.is_valid():
# Update/create feature # Update/create mapitem
commit_type = 'Created' if feature is None else 'Updated' commit_type = 'Created' if mapitem is None else 'Updated'
action = 'create' if feature is None else 'edit' action = 'create' if mapitem is None else 'edit'
feature = form.instance mapitem = form.instance
if form.titles is not None: if form.titles is not None:
feature.titles = {} mapitem.titles = {}
for language, title in form.titles.items(): for language, title in form.titles.items():
if title: if title:
feature.titles[language] = title mapitem.titles[language] = title
if not settings.DIRECT_EDITING: if not settings.DIRECT_EDITING:
content = json_encode(feature.tofile()) content = json_encode(mapitem.tofile())
with translation.override('en'): with translation.override('en'):
commit_msg = '%s %s: %s' % (commit_type, model._meta.verbose_name, feature.title) commit_msg = '%s %s: %s' % (commit_type, mapitemtype._meta.verbose_name, mapitem.title)
return render(request, 'editor/feature_success.html', { return render(request, 'editor/mapitem_success.html', {
'data': signing.dumps({ 'data': signing.dumps({
'type': 'editor.edit', 'type': 'editor.edit',
'action': action, 'action': action,
'package_name': feature.package.name, 'package_name': mapitem.package.name,
'commit_id': feature.package.commit_id, 'commit_id': mapitem.package.commit_id,
'commit_msg': commit_msg, 'commit_msg': commit_msg,
'file_path': feature.get_filename(), 'file_path': mapitem.get_filename(),
'content': content, 'content': content,
}) })
}) })
feature.save() mapitem.save()
return render(request, 'editor/feature_success.html', {}) return render(request, 'editor/mapitem_success.html', {
'mapitem_type': mapitem_type
})
else: else:
form = model.EditorForm(instance=feature, request=request) form = mapitemtype.EditorForm(instance=mapitem, request=request)
return render(request, 'editor/feature.html', { return render(request, 'editor/mapitem.html', {
'form': form, 'form': form,
'feature_type': feature_type, 'mapitem_type': mapitem_type,
'has_geometry': hasattr(mapitemtype, 'geometry'),
'name': orig_name,
'geomtype': getattr(mapitemtype, 'geomtype', None),
'path': request.path, 'path': request.path,
'new': feature is None 'new': new
}) })

View file

@ -39,7 +39,7 @@ class GeometryViewSet(ViewSet):
else: else:
types = [t for t in types if t in valid_types] types = [t for t in types if t in valid_types]
levels = request.GET.getlist('levels') levels = request.GET.getlist('level')
packages = request.GET.getlist('package') packages = request.GET.getlist('package')
names = request.GET.getlist('name') names = request.GET.getlist('name')

View file

@ -1,5 +1,4 @@
from c3nav.mapdata.models.geometry import GEOMETRY_MAPITEM_TYPES # noqa
from .level import Level # noqa from .level import Level # noqa
from .package import Package # noqa from .package import Package # noqa
from .source import Source # noqa from .source import Source # noqa
from .geometry import GeometryMapItem # noqa from .geometry import GeometryMapItem, GEOMETRY_MAPITEM_TYPES # noqa

View file

@ -1,10 +1,21 @@
from collections import OrderedDict from collections import OrderedDict
from django.db import models from django.db import models
from django.db.models.base import ModelBase
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
MAPITEM_TYPES = OrderedDict()
class MapItem(models.Model):
class MapItemMeta(ModelBase):
def __new__(mcs, name, bases, attrs):
cls = super().__new__(mcs, name, bases, attrs)
if not cls._meta.abstract:
MAPITEM_TYPES[name.lower()] = cls
return cls
class MapItem(models.Model, metaclass=MapItemMeta):
name = models.SlugField(_('Name'), unique=True, max_length=50) name = models.SlugField(_('Name'), unique=True, max_length=50)
package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, verbose_name=_('map package')) package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, verbose_name=_('map package'))

View file

@ -1,18 +1,17 @@
from collections import OrderedDict from collections import OrderedDict
from django.db import models from django.db import models
from django.db.models.base import ModelBase
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from shapely.geometry.geo import mapping, shape from shapely.geometry.geo import mapping, shape
from c3nav.mapdata.fields import GeometryField from c3nav.mapdata.fields import GeometryField
from c3nav.mapdata.models.base import MapItem from c3nav.mapdata.models.base import MapItem, MapItemMeta
from c3nav.mapdata.utils import format_geojson from c3nav.mapdata.utils import format_geojson
GEOMETRY_MAPITEM_TYPES = OrderedDict() GEOMETRY_MAPITEM_TYPES = OrderedDict()
class GeometryMapItemMeta(ModelBase): class GeometryMapItemMeta(MapItemMeta):
def __new__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):
cls = super().__new__(mcs, name, bases, attrs) cls = super().__new__(mcs, name, bases, attrs)
if not cls._meta.abstract: if not cls._meta.abstract: