diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 5e40dea6..9fb81f07 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -8,8 +8,8 @@ from django.conf import settings from django.core.cache import cache from django.core.exceptions import FieldDoesNotExist from django.db.models import Q -from django.forms import (BooleanField, CharField, ChoiceField, Form, ModelChoiceField, ModelForm, MultipleChoiceField, - Select, ValidationError) +from django.forms import (BooleanField, CharField, ChoiceField, DecimalField, Form, ModelChoiceField, ModelForm, + MultipleChoiceField, Select, ValidationError) from django.forms.widgets import HiddenInput from django.utils.translation import ugettext_lazy as _ from shapely.geometry.geo import mapping @@ -52,6 +52,26 @@ class EditorFormBase(I18nModelFormMixin, ModelForm): all_names = set(os.listdir(settings.SOURCES_ROOT)) self.fields['name'].widget = Select(choices=tuple((s, s) for s in sorted(all_names-used_names))) + self.fields['fixed_x'] = DecimalField(label='fixed x', required=False, + max_digits=7, decimal_places=3, initial=0) + self.fields['fixed_y'] = DecimalField(label='fixed y', required=False, + max_digits=7, decimal_places=3, initial=0) + self.fields['scale_x'] = DecimalField(label='scale x (m/px)', required=False, + max_digits=7, decimal_places=3, initial=1) + self.fields['scale_y'] = DecimalField(label='scale y (m/px)', required=False, + max_digits=7, decimal_places=3, initial=1) + self.fields['lock_aspect'] = BooleanField(label='lock aspect ratio', required=False, initial=True) + self.fields['lock_scale'] = BooleanField(label='lock scale (for moving)', required=False, initial=True) + + self.fields.move_to_end('lock_scale', last=False) + self.fields.move_to_end('lock_aspect', last=False) + self.fields.move_to_end('scale_y', last=False) + self.fields.move_to_end('scale_x', last=False) + self.fields.move_to_end('fixed_y', last=False) + self.fields.move_to_end('fixed_x', last=False) + self.fields.move_to_end('access_restriction', last=False) + self.fields.move_to_end('name', last=False) + if self._meta.model.__name__ == 'AccessRestriction': AccessRestrictionGroup = self.request.changeset.wrap_model('AccessRestrictionGroup') diff --git a/src/c3nav/editor/static/editor/css/editor.css b/src/c3nav/editor/static/editor/css/editor.css index f38ee21e..0030638b 100644 --- a/src/c3nav/editor/static/editor/css/editor.css +++ b/src/c3nav/editor/static/editor/css/editor.css @@ -52,7 +52,7 @@ body:not(.map-enabled) #sidebar { padding:12px 12px 0; margin:auto; } -#sidebar .content form .form-group:last-child { +#sidebar .content form>.form-group:last-child { margin-bottom:0; padding:0 0 12px; position:sticky; @@ -61,6 +61,18 @@ body:not(.map-enabled) #sidebar { background-color: #ffffff; box-shadow: 0 0 10px 10px #ffffff; } +#sidebar .form-group-group { + display: flex; +} +#sidebar .form-group-group .form-group { + flex-grow: 1; +} +#sidebar .form-group-group .form-group:not(:last-child) { + margin-right: 15px; +} +#sidebar form:not(.show-source-wizard) .source-wizard { + display: none; +} #noscript { font-weight:bold; color:red; diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index 5a56083e..196bf925 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -9,7 +9,7 @@ editor = { renderer: L.svg({ padding: 2 }), zoom: 2, maxZoom: 10, - minZoom: 0, + minZoom: -5, crs: L.CRS.Simple, editable: true, zoomSnap: 0 @@ -150,6 +150,51 @@ editor = { $('#navbar-collapse').find('.nav').html(nav.html()); } + if (editor._source_image_layer) { + editor._source_image_layer.remove(); + editor._source_image_layer = null; + } + + var group; + if (content.find('[name=fixed_x]')) { + $('[name=name]').change(editor._source_name_selected).change(); + if (!content.find('[data-new]').length) { + var bounds = [[parseFloat(content.find('[name=left]').val()), parseFloat(content.find('[name=bottom]').val())], [parseFloat(content.find('[name=right]').val()), parseFloat(content.find('[name=top]').val())]]; + bounds = L.GeoJSON.coordsToLatLngs(bounds); + editor.map.fitBounds(bounds, {padding: [30, 50]}); + } + + group = $('
'); + group.insertBefore(content.find('[name=fixed_x]').closest('.form-group')); + group.append(content.find('[name=fixed_x]').closest('.form-group')); + group.append(content.find('[name=fixed_y]').closest('.form-group')); + + group = $('
'); + group.insertBefore(content.find('[name=scale_x]').closest('.form-group')); + group.append(content.find('[name=scale_x]').closest('.form-group')); + group.append(content.find('[name=scale_y]').closest('.form-group')); + + content.find('[name=left], [name=bottom], [name=right], [name=top]').change(editor._source_image_bounds_changed); + content.find('[name=scale_x], [name=scale_y]').change(editor._source_image_scale_changed); + content.find('[name=left], [name=bottom], [name=right], [name=top]').each(function() { $(this).data('oldval', $(this).val()); }); + + content.find('[name=lock_aspect], [name=lock_scale]').closest('.form-group').addClass('source-wizard'); + + var source_width = (parseFloat(content.find('[name=right]').val()) || 0) - (parseFloat(content.find('[name=left]').val()) || 0), + source_height = (parseFloat(content.find('[name=top]').val()) || 0) - (parseFloat(content.find('[name=bottom]').val()) || 0); + editor._source_aspect_ratio = source_width/(source_height || 1); + } + if (content.find('[name=left]')) { + group = $('
'); + group.insertBefore(content.find('[name=left]').closest('.form-group')); + group.append(content.find('[name=left]').closest('.form-group')); + group.append(content.find('[name=top]').closest('.form-group')); + group = $('
'); + group.insertBefore(content.find('[name=right]').closest('.form-group')); + group.append(content.find('[name=right]').closest('.form-group')); + group.append(content.find('[name=bottom]').closest('.form-group')); + } + content.find('[data-toggle="tooltip"]').tooltip(); var modal_close = content.find('[data-modal-close]'); @@ -292,6 +337,148 @@ editor = { $.post(action, data, editor._sidebar_loaded).fail(editor._sidebar_error); }, + _source_image_orig_width: 0, + _source_image_orig_height: 0, + _source_image_aspect_ratio: 0, + _source_image_untouched: 0, + _source_image_layer: null, + _source_name_selected: function() { + if (editor._source_image_layer) { + editor._source_image_layer.remove(); + editor._source_image_layer = null; + } + $('').on('load', editor._source_name_selected_ajax_callback); + $('#sidebar form').removeClass('show-source-wizard'); + $('body').removeClass('map-enabled'); + }, + _source_name_selected_ajax_callback: function() { + if ($(this).attr('src').endsWith($('#sidebar [name=name]').val())) { + $('#sidebar form').addClass('show-source-wizard'); + $(this).appendTo('body').hide(); + editor._source_image_orig_width = $(this).width(); + editor._source_image_orig_height = $(this).height(); + $(this).remove(); + $('body').addClass('map-enabled'); + var content = $('#sidebar'); + if (content.find('[data-new]').length || isNaN(parseFloat(content.find('[name=right]').val())) || isNaN(parseFloat(content.find('[name=left]').val())) || isNaN(parseFloat(content.find('[name=top]').val())) || isNaN(parseFloat(content.find('[name=bottom]').val()))) { + editor._source_aspect_ratio = $(this).width()/$(this).height(); + content.find('[name=left]').val(0).data('oldval', 0); + content.find('[name=bottom]').val(0).data('oldval', 0); + var factor = 1; + while(factor < 1000 && (editor._source_image_orig_width/factor)>1500) { + factor *= 10; + } + var width = (editor._source_image_orig_width/factor).toFixed(2), + height = (editor._source_image_orig_height/factor).toFixed(2); + content.find('[name=right]').val(width).data('oldval', width); + content.find('[name=top]').val(height).data('oldval', height); + content.find('[name=scale_x]').val(1/factor); + content.find('[name=scale_y]').val(1/factor); + } else { + editor._source_image_calculate_scale(); + } + editor._source_image_repositioned(); + } + }, + _source_image_repositioned: function() { + var content = $('#sidebar'); + if (isNaN(parseFloat(content.find('[name=right]').val())) || isNaN(parseFloat(content.find('[name=left]').val())) || isNaN(parseFloat(content.find('[name=top]').val())) || isNaN(parseFloat(content.find('[name=bottom]').val()))) { + return; + } + var bounds = [[parseFloat(content.find('[name=left]').val()), parseFloat(content.find('[name=bottom]').val())], [parseFloat(content.find('[name=right]').val()), parseFloat(content.find('[name=top]').val())]]; + bounds = L.GeoJSON.coordsToLatLngs(bounds); + + editor._set_max_bounds(bounds); + if (editor._source_image_layer) { + editor._source_image_layer.setBounds(bounds) + } else { + editor._source_image_layer = L.imageOverlay('/editor/sourceimage/'+content.find('[name=name]').val(), bounds, {opacity: 0.3, zIndex: 10000}); + editor._source_image_layer.addTo(editor.map); + if (content.find('[data-new]').length) { + editor.map.fitBounds(bounds, {padding: [30, 50]}); + } + } + }, + _source_image_calculate_scale: function() { + var content = $('#sidebar'); + var source_width = parseFloat(content.find('[name=right]').val()) - parseFloat(content.find('[name=left]').val()), + source_height = parseFloat(content.find('[name=top]').val()) - parseFloat(content.find('[name=bottom]').val()); + if (isNaN(source_width) || isNaN(source_height)) return; + var scale_x = (source_width/editor._source_image_orig_width).toFixed(3), + scale_y = (source_height/editor._source_image_orig_height).toFixed(3); + content.find('[name=scale_x]').val(scale_x); + content.find('[name=scale_y]').val(scale_y); + if (scale_x !== scale_y) { + content.find('[name=lock_aspect]').prop('checked', false); + } + }, + _source_image_bounds_changed: function() { + var content = $('#sidebar'), + lock_scale = content.find('[name=lock_scale]').prop('checked'), + oldval = $(this).data('oldval'), + newval = $(this).val(), + diff = parseFloat(newval)-parseFloat(oldval); + $(this).data('oldval', newval); + if (lock_scale) { + if (!isNaN(diff)) { + var other_field_name = {left: 'right', right: 'left', top: 'bottom', bottom: 'top'}[$(this).attr('name')], + other_field = content.find('[name='+other_field_name+']'), + other_val = parseFloat(other_field.val()); + if (!isNaN(other_val)) { + other_field.val((other_val+diff).toFixed(2)).data('oldval', other_val); + } + } + } else { + editor._source_image_calculate_scale(); + } + editor._source_image_repositioned(); + }, + _source_image_scale_changed: function() { + var content = $('#sidebar'), + lock_aspect = content.find('[name=lock_scale]').prop('checked'); + if (lock_aspect) { + var other_field_name = {scale_x: 'scale_y', scale_y: 'scale_x'}[$(this).attr('name')], + other_field = content.find('[name='+other_field_name+']'); + other_field.val($(this).val()); + } + var f_scale_x = content.find('[name=scale_x]'), + f_scale_y = content.find('[name=scale_y]'), + scale_x = f_scale_x.val(), + scale_y = f_scale_y.val(), + fixed_x = parseFloat(content.find('[name=fixed_x]').val()), + fixed_y = parseFloat(content.find('[name=fixed_y]').val()), + left = parseFloat(content.find('[name=left]').val()), + bottom = parseFloat(content.find('[name=bottom]').val()), + right = parseFloat(content.find('[name=right]').val()), + top = parseFloat(content.find('[name=top]').val()); + + scale_x = parseFloat(scale_x); + scale_y = parseFloat(scale_y); + + if (isNaN(scale_x) || isNaN(scale_y) || isNaN(fixed_x) || isNaN(fixed_y) || isNaN(left) || isNaN(bottom) || isNaN(right) || isNaN(top)) return; + + var fixed_x_relative = (fixed_x-left)/(right-left), + fixed_y_relative = (fixed_y-bottom)/(top-bottom), + width = editor._source_image_orig_width*scale_x, + height = editor._source_image_orig_height*scale_y, + left = fixed_x-(width*fixed_x_relative), + bottom = fixed_y-(height*fixed_y_relative), + right = left+width, + top = bottom+height; + + left = left.toFixed(2); + bottom = bottom.toFixed(2); + right = right.toFixed(2); + top = top.toFixed(2); + + content.find('[name=left]').val(left).data('oldval', left); + content.find('[name=bottom]').val(bottom).data('oldval', bottom); + content.find('[name=right]').val(right).data('oldval', right); + content.find('[name=top]').val(top).data('oldval', top); + + editor._source_image_repositioned(); + }, + // geometries geometrystyles: {}, _loading_geometry: false, @@ -314,6 +501,7 @@ editor = { _arrow_colors: [], _last_vertex: null, _orig_vertex_pos: null, + _max_bounds: null, init_geometries: function () { // init geometries and edit listeners editor._highlight_layer = L.layerGroup().addTo(editor.map); @@ -376,13 +564,18 @@ editor = { editor.geometrystyles = geometrystyles; $.getJSON('/api/editor/bounds/', function(bounds) { bounds = L.GeoJSON.coordsToLatLngs(bounds.bounds); - editor.map.setMaxBounds(bounds); + editor._max_bounds = bounds; + editor._set_max_bounds(); editor.map.fitBounds(bounds, {padding: [30, 50]}); editor.init_sidebar(); }); }); editor.get_sources(); }, + _set_max_bounds: function(bounds) { + bounds = bounds ? L.latLngBounds(editor._max_bounds[0], editor._max_bounds[1]).extend(bounds) : editor._max_bounds; + editor.map.setMaxBounds(bounds); + }, _last_geometry_url: null, load_geometries: function (geometry_url, highlight_type, editing_id) { // load geometries from url @@ -400,6 +593,7 @@ editor = { editor._graph_edges_from = {}; editor._graph_edges_to = {}; + editor._set_max_bounds(); $.getJSON(geometry_url, function(geometries) { editor.map.removeLayer(editor._highlight_layer); editor._highlight_layer.clearLayers();