data overlays
This commit is contained in:
parent
60de7857d6
commit
7904a95b80
22 changed files with 1230 additions and 219 deletions
|
@ -53,6 +53,7 @@ def geometrystyles(request):
|
|||
'altitudemarker': '#0000FF',
|
||||
'beaconmeasurement': '#DDDD00',
|
||||
'rangingbeacon': '#CC00CC',
|
||||
'dataoverlayfeature': '#3366ff',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -52,6 +52,9 @@ def _get_geometries_for_one_level(level):
|
|||
results.append(door)
|
||||
|
||||
results.extend(sorted(spaces.values(), key=space_sorting_func))
|
||||
|
||||
results.extend(level.data_overlay_features.all())
|
||||
|
||||
return results
|
||||
|
||||
|
||||
|
@ -121,6 +124,7 @@ def get_level_geometries_result(request, level_id: int, update_cache_key: str, u
|
|||
LocationGroup = request.changeset.wrap_model('LocationGroup')
|
||||
BeaconMeasurement = request.changeset.wrap_model('BeaconMeasurement')
|
||||
RangingBeacon = request.changeset.wrap_model('RangingBeacon')
|
||||
DataOverlayFeature = request.changeset.wrap_model('DataOverlayFeature')
|
||||
|
||||
try:
|
||||
level = Level.objects.filter(Level.q_for_request(request)).get(pk=level_id)
|
||||
|
@ -151,7 +155,8 @@ def get_level_geometries_result(request, level_id: int, update_cache_key: str, u
|
|||
Prefetch('spaces__altitudemarkers', AltitudeMarker.objects.only('geometry', 'space')),
|
||||
Prefetch('spaces__beacon_measurements', BeaconMeasurement.objects.only('geometry', 'space')),
|
||||
Prefetch('spaces__ranging_beacons', RangingBeacon.objects.only('geometry', 'space')),
|
||||
Prefetch('spaces__graphnodes', graphnodes_qs)
|
||||
Prefetch('spaces__graphnodes', graphnodes_qs),
|
||||
Prefetch('data_overlay_features', DataOverlayFeature.objects.only('geometry', 'overlay_id', 'level'))
|
||||
)
|
||||
|
||||
levels = {s.pk: s for s in levels}
|
||||
|
|
|
@ -63,6 +63,7 @@ class EditorGeometriesPropertiesSchema(BaseSchema):
|
|||
Annotated[str, APIField(title="color")],
|
||||
Annotated[None, APIField(title="no color")]
|
||||
] = None
|
||||
overlay: Optional[EditorID] = None
|
||||
opacity: Optional[float] = None # todo: range
|
||||
|
||||
|
||||
|
|
|
@ -439,6 +439,8 @@ def create_editor_form(editor_model):
|
|||
'icon_path', 'leaflet_marker_config',
|
||||
'color_background', 'color_wall_fill', 'color_wall_border', 'color_door_fill',
|
||||
'color_ground_fill', 'color_obstacles_default_fill', 'color_obstacles_default_border',
|
||||
'stroke_color', 'stroke_width', 'fill_color', 'interactive', 'point_icon', 'extra_data',
|
||||
'show_label', 'show_geometry', 'external_url',
|
||||
]
|
||||
field_names = [field.name for field in editor_model._meta.get_fields() if not field.one_to_many]
|
||||
existing_fields = [name for name in possible_fields if name in field_names]
|
||||
|
|
|
@ -6,7 +6,7 @@ editor = {
|
|||
init: function () {
|
||||
// Init Map
|
||||
editor.map = L.map('map', {
|
||||
renderer: L.svg({ padding: 2 }),
|
||||
renderer: L.svg({padding: 2}),
|
||||
zoom: 2,
|
||||
maxZoom: 10,
|
||||
minZoom: -5,
|
||||
|
@ -55,11 +55,11 @@ editor = {
|
|||
|
||||
L.control.scale({imperial: false}).addTo(editor.map);
|
||||
|
||||
$('#show_map').click(function(e) {
|
||||
$('#show_map').click(function (e) {
|
||||
e.preventDefault();
|
||||
$('body').addClass('show-map');
|
||||
});
|
||||
$('#show_details').click(function(e) {
|
||||
$('#show_details').click(function (e) {
|
||||
e.preventDefault();
|
||||
$('body').removeClass('show-map');
|
||||
});
|
||||
|
@ -74,7 +74,7 @@ editor = {
|
|||
editor.init_scancollector();
|
||||
editor.sidebar_content_loaded();
|
||||
},
|
||||
_inform_mobile_client: function(elem) {
|
||||
_inform_mobile_client: function (elem) {
|
||||
if (!window.mobileclient || !elem.length) return;
|
||||
var data = JSON.parse(elem.attr('data-user-data'));
|
||||
data.changes_count_display = elem.attr('data-count-display');
|
||||
|
@ -82,7 +82,7 @@ editor = {
|
|||
data.has_changeset = elem.is('[data-has-changeset]');
|
||||
mobileclient.setUserData(JSON.stringify(data));
|
||||
},
|
||||
_onbeforeunload: function(e) {
|
||||
_onbeforeunload: function (e) {
|
||||
if ($('#sidebar').find('[data-onbeforeunload]').length) {
|
||||
e.returnValue = true;
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ editor = {
|
|||
get_location_path: function () {
|
||||
return window.location.pathname + window.location.search;
|
||||
},
|
||||
init_sidebar: function() {
|
||||
init_sidebar: function () {
|
||||
// init the sidebar. sed listeners for form submits and link clicks
|
||||
$('#sidebar').find('.content').on('click', 'a[href]', editor._sidebar_link_click)
|
||||
.on('click', 'button[type=submit]', editor._sidebar_submit_btn_click)
|
||||
|
@ -151,17 +151,17 @@ editor = {
|
|||
var location_path = editor.get_location_path();
|
||||
editor._sidebar_loaded();
|
||||
history.replaceState({}, '', location_path);
|
||||
window.onpopstate = function() {
|
||||
window.onpopstate = function () {
|
||||
editor.sidebar_get(editor.get_location_path(), true);
|
||||
};
|
||||
},
|
||||
sidebar_get: function(location, no_push) {
|
||||
sidebar_get: function (location, no_push) {
|
||||
// load a new page into the sidebar using a GET request
|
||||
if (!no_push) history.pushState({}, '', location);
|
||||
editor._sidebar_unload();
|
||||
$.get(location, editor._sidebar_loaded).fail(editor._sidebar_error);
|
||||
},
|
||||
_sidebar_unload: function() {
|
||||
_sidebar_unload: function () {
|
||||
// unload the sidebar. called on sidebar_get and form submit.
|
||||
editor._level_control.disable();
|
||||
editor._sublevel_control.disable();
|
||||
|
@ -202,12 +202,15 @@ editor = {
|
|||
level_control.current_id = parseInt(level_list.attr('data-current-id'));
|
||||
},
|
||||
|
||||
sidebar_content_loaded: function() {
|
||||
sidebar_content_loaded: function () {
|
||||
if (document.querySelector('#sidebar [data-themed-color]')) {
|
||||
editor.theme_editor_loaded();
|
||||
}
|
||||
if (document.querySelector('TODO')) {
|
||||
|
||||
}
|
||||
},
|
||||
theme_editor_loaded: function() {
|
||||
theme_editor_loaded: function () {
|
||||
const filter_show_all = () => {
|
||||
for (const input of document.querySelectorAll('#sidebar [data-themed-color]')) {
|
||||
input.parentElement.classList.remove('theme-color-hidden');
|
||||
|
@ -263,7 +266,8 @@ editor = {
|
|||
},
|
||||
|
||||
_in_modal: false,
|
||||
_sidebar_loaded: function(data) {
|
||||
sidebar_extra_data: {},
|
||||
_sidebar_loaded: function (data) {
|
||||
// sidebar was loaded. load the content. check if there are any redirects. call _check_start_editing.
|
||||
var content = $('#sidebar').removeClass('loading').find('.content');
|
||||
if (data !== undefined) {
|
||||
|
@ -287,6 +291,14 @@ editor = {
|
|||
|
||||
editor._beacon_layer.clearLayers();
|
||||
|
||||
const extraData = content.find('#sidebar-extra-data').first().text();
|
||||
|
||||
if (extraData) {
|
||||
editor.sidebar_extra_data = JSON.parse(extraData);
|
||||
} else {
|
||||
editor.sidebar_extra_data = null;
|
||||
}
|
||||
|
||||
var group;
|
||||
if (content.find('[name=fixed_x]').length) {
|
||||
$('[name=name]').change(editor._source_name_selected).change();
|
||||
|
@ -311,13 +323,15 @@ editor = {
|
|||
|
||||
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=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);
|
||||
editor._source_aspect_ratio = source_width / (source_height || 1);
|
||||
}
|
||||
if (content.find('[name=left]').length) {
|
||||
group = $('<div class="form-group-group">');
|
||||
|
@ -402,8 +416,8 @@ editor = {
|
|||
var level_control_offset = $(editor._level_control_container).position();
|
||||
var offset_parent = $(editor._level_control_container).offsetParent();
|
||||
$(editor._sublevel_control._container).css({
|
||||
bottom: offset_parent.outerHeight()-level_control_offset.top-editor._level_control_container.outerHeight()-parseInt(editor._level_control_container.css('margin-bottom')),
|
||||
right: offset_parent.outerWidth()-level_control_offset.left
|
||||
bottom: offset_parent.outerHeight() - level_control_offset.top - editor._level_control_container.outerHeight() - parseInt(editor._level_control_container.css('margin-bottom')),
|
||||
right: offset_parent.outerWidth() - level_control_offset.left
|
||||
});
|
||||
} else {
|
||||
$body.removeClass('show-map');
|
||||
|
@ -433,12 +447,12 @@ editor = {
|
|||
data_field.after(collector);
|
||||
}
|
||||
},
|
||||
_sidebar_error: function(data) {
|
||||
$('#sidebar').removeClass('loading').find('.content').html('<h3>Error '+data.status+'</h3>'+data.statusText);
|
||||
_sidebar_error: function (data) {
|
||||
$('#sidebar').removeClass('loading').find('.content').html('<h3>Error ' + data.status + '</h3>' + data.statusText);
|
||||
editor._level_control.hide();
|
||||
editor._sublevel_control.hide();
|
||||
},
|
||||
_sidebar_link_click: function(e) {
|
||||
_sidebar_link_click: function (e) {
|
||||
// listener for link-clicks in the sidebar.
|
||||
var href = $(this).attr('href');
|
||||
if (href && !href.startsWith('/editor/')) return;
|
||||
|
@ -449,14 +463,14 @@ editor = {
|
|||
if ($(this).is('[data-no-next-zoom]')) editor._next_zoom = false;
|
||||
editor.sidebar_get($(this).attr('href'));
|
||||
},
|
||||
_sidebar_submit_btn_click: function() {
|
||||
_sidebar_submit_btn_click: function () {
|
||||
// listener for submit-button-clicks in the sidebar, so the submit event will know which button submitted.
|
||||
if (editor._loading_geometry) return;
|
||||
$(this).closest('form').data('btn', $(this)).clearQueue().delay(300).queue(function() {
|
||||
$(this).closest('form').data('btn', $(this)).clearQueue().delay(300).queue(function () {
|
||||
$(this).data('btn', null);
|
||||
});
|
||||
},
|
||||
_sidebar_submit: function(e) {
|
||||
_sidebar_submit: function (e) {
|
||||
// listener for form submits in the sidebar.
|
||||
e.preventDefault();
|
||||
if (editor._loading_geometry || $(this).is('.creation-lock') || $(this).is('.scan-lock')) return;
|
||||
|
@ -485,17 +499,17 @@ editor = {
|
|||
_source_image_aspect_ratio: 0,
|
||||
_source_image_untouched: 0,
|
||||
_source_image_layer: null,
|
||||
_source_name_selected: function() {
|
||||
_source_name_selected: function () {
|
||||
if (editor._source_image_layer) {
|
||||
editor._source_image_layer.remove();
|
||||
editor._source_image_layer = null;
|
||||
}
|
||||
// noinspection HtmlRequiredAltAttribute
|
||||
$('<img src="/editor/sourceimage/'+$(this).val()+'">').on('load', editor._source_name_selected_ajax_callback);
|
||||
$('<img src="/editor/sourceimage/' + $(this).val() + '">').on('load', editor._source_name_selected_ajax_callback);
|
||||
$('#sidebar form').removeClass('show-source-wizard');
|
||||
$('body').removeClass('map-enabled');
|
||||
},
|
||||
_source_name_selected_ajax_callback: function() {
|
||||
_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();
|
||||
|
@ -505,26 +519,26 @@ editor = {
|
|||
$('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();
|
||||
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) {
|
||||
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);
|
||||
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);
|
||||
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() {
|
||||
_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;
|
||||
|
@ -536,40 +550,48 @@ editor = {
|
|||
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 = 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() {
|
||||
_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);
|
||||
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() {
|
||||
_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);
|
||||
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+']'),
|
||||
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);
|
||||
other_field.val((other_val + diff).toFixed(2)).data('oldval', other_val);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -577,12 +599,12 @@ editor = {
|
|||
}
|
||||
editor._source_image_repositioned();
|
||||
},
|
||||
_source_image_scale_changed: function() {
|
||||
_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 = content.find('[name=' + other_field_name + ']');
|
||||
other_field.val($(this).val());
|
||||
}
|
||||
var f_scale_x = content.find('[name=scale_x]'),
|
||||
|
@ -601,14 +623,14 @@ editor = {
|
|||
|
||||
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;
|
||||
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);
|
||||
|
@ -622,7 +644,7 @@ editor = {
|
|||
|
||||
editor._source_image_repositioned();
|
||||
},
|
||||
_fixed_point_changed: function() {
|
||||
_fixed_point_changed: function () {
|
||||
var content = $('#sidebar'),
|
||||
fixed_x = parseFloat(content.find('[name=fixed_x]').val()),
|
||||
fixed_y = parseFloat(content.find('[name=fixed_y]').val()),
|
||||
|
@ -637,7 +659,7 @@ editor = {
|
|||
editor._fixed_point_layer = null;
|
||||
}
|
||||
} else if (valid) {
|
||||
editor._fixed_point_layer = L.marker(latlng, {draggable: true, autoPan: true}).on('dragend', function(e) {
|
||||
editor._fixed_point_layer = L.marker(latlng, {draggable: true, autoPan: true}).on('dragend', function (e) {
|
||||
var coords = L.GeoJSON.latLngToCoords(e.target.getLatLng());
|
||||
content.find('[name=fixed_x]').val(coords[0].toFixed(3));
|
||||
content.find('[name=fixed_y]').val(coords[1].toFixed(3));
|
||||
|
@ -645,14 +667,14 @@ editor = {
|
|||
editor._fixed_point_layer.addTo(editor.map);
|
||||
}
|
||||
},
|
||||
_copy_from_changed: function() {
|
||||
_copy_from_changed: function () {
|
||||
var content = $('#sidebar'),
|
||||
value = JSON.parse($(this).val());
|
||||
$(this).val('');
|
||||
if (!confirm('Are you sure you want to copy settings from '+value.name+'?')) return;
|
||||
if (!confirm('Are you sure you want to copy settings from ' + value.name + '?')) return;
|
||||
delete value.name;
|
||||
for (var key in value) {
|
||||
if (value.hasOwnProperty(key)) content.find('[name='+key+']').val(value[key]);
|
||||
if (value.hasOwnProperty(key)) content.find('[name=' + key + ']').val(value[key]);
|
||||
}
|
||||
editor._source_image_calculate_scale();
|
||||
editor._source_image_repositioned();
|
||||
|
@ -699,27 +721,27 @@ editor = {
|
|||
editor.map.on('editable:vertex:click', function () {
|
||||
editor.map.doubleClickZoom.disable();
|
||||
});
|
||||
editor.map.on('editable:drawing:start editable:drawing:end', function() {
|
||||
editor.map.on('editable:drawing:start editable:drawing:end', function () {
|
||||
editor._last_vertex = null;
|
||||
editor._num_vertices = 0;
|
||||
});
|
||||
editor.map.on('editable:vertex:new', function(e) {
|
||||
editor.map.on('editable:vertex:new', function (e) {
|
||||
if (editor._shift_pressed && editor._creating && editor._creating_type === 'polygon' && editor._num_vertices === 1) {
|
||||
var firstPoint = new L.Point(editor._last_vertex.latlng.lng, editor._last_vertex.latlng.lat),
|
||||
secondPoint = new L.Point(e.vertex.latlng.lng, e.vertex.latlng.lat),
|
||||
center = new L.Point((firstPoint.x+secondPoint.x)/2, (firstPoint.y+secondPoint.y)/2),
|
||||
radius = firstPoint.distanceTo(secondPoint)/2,
|
||||
center = new L.Point((firstPoint.x + secondPoint.x) / 2, (firstPoint.y + secondPoint.y) / 2),
|
||||
radius = firstPoint.distanceTo(secondPoint) / 2,
|
||||
options = e.layer.options,
|
||||
points = Math.min(32, 8+Math.floor(radius*5)*2),
|
||||
points = Math.min(32, 8 + Math.floor(radius * 5) * 2),
|
||||
vertices = [];
|
||||
for (var i=0;i<points;i++) {
|
||||
for (var i = 0; i < points; i++) {
|
||||
vertices.push([
|
||||
center.x+Math.sin(Math.PI*2/points*i)*radius,
|
||||
center.y+Math.cos(Math.PI*2/points*i)*radius
|
||||
center.x + Math.sin(Math.PI * 2 / points * i) * radius,
|
||||
center.y + Math.cos(Math.PI * 2 / points * i) * radius
|
||||
])
|
||||
}
|
||||
var polygon = L.polygon(L.GeoJSON.coordsToLatLngs(vertices), options).addTo(editor.map);
|
||||
window.setTimeout(function() {
|
||||
window.setTimeout(function () {
|
||||
editor.map.editTools.stopDrawing();
|
||||
polygon.enableEdit();
|
||||
editor._done_creating({layer: polygon});
|
||||
|
@ -740,11 +762,11 @@ editor = {
|
|||
dy = e.latlng.lat - editor._orig_vertex_pos[0],
|
||||
angle = Math.atan2(dy, dx) * (180 / Math.PI),
|
||||
distance = Math.hypot(dx, dy),
|
||||
newangle = Math.round(angle/15)*15 / (180 / Math.PI);
|
||||
e.latlng.lat = editor._orig_vertex_pos[0] + Math.sin(newangle)*distance;
|
||||
e.latlng.lng = editor._orig_vertex_pos[1] + Math.cos(newangle)*distance;
|
||||
newangle = Math.round(angle / 15) * 15 / (180 / Math.PI);
|
||||
e.latlng.lat = editor._orig_vertex_pos[0] + Math.sin(newangle) * distance;
|
||||
e.latlng.lng = editor._orig_vertex_pos[1] + Math.cos(newangle) * distance;
|
||||
}
|
||||
e.vertex.setLatLng([Math.round(e.latlng.lat*100)/100, Math.round(e.latlng.lng*100)/100]);
|
||||
e.vertex.setLatLng([Math.round(e.latlng.lat * 100) / 100, Math.round(e.latlng.lng * 100) / 100]);
|
||||
});
|
||||
editor.map.on('editable:drawing:click editable:drawing:move', function (e) {
|
||||
if (e.originalEvent.ctrlKey && editor._last_vertex) {
|
||||
|
@ -752,12 +774,12 @@ editor = {
|
|||
dy = e.latlng.lat - editor._last_vertex.latlng.lat,
|
||||
angle = Math.atan2(dy, dx) * (180 / Math.PI),
|
||||
distance = Math.hypot(dx, dy),
|
||||
newangle = Math.round(angle/15)*15 / (180 / Math.PI);
|
||||
e.latlng.lat = editor._last_vertex.latlng.lat + Math.sin(newangle)*distance;
|
||||
e.latlng.lng = editor._last_vertex.latlng.lng + Math.cos(newangle)*distance;
|
||||
newangle = Math.round(angle / 15) * 15 / (180 / Math.PI);
|
||||
e.latlng.lat = editor._last_vertex.latlng.lat + Math.sin(newangle) * distance;
|
||||
e.latlng.lng = editor._last_vertex.latlng.lng + Math.cos(newangle) * distance;
|
||||
}
|
||||
e.latlng.lat = Math.round(e.latlng.lat*100)/100;
|
||||
e.latlng.lng = Math.round(e.latlng.lng*100)/100;
|
||||
e.latlng.lat = Math.round(e.latlng.lat * 100) / 100;
|
||||
e.latlng.lng = Math.round(e.latlng.lng * 100) / 100;
|
||||
});
|
||||
editor.map.on('editable:drawing:click', function (e) {
|
||||
editor._shift_pressed = e.originalEvent.altKey;
|
||||
|
@ -786,7 +808,7 @@ editor = {
|
|||
|
||||
editor._beacon_layer = L.layerGroup().addTo(editor.map);
|
||||
},
|
||||
_set_max_bounds: function(bounds) {
|
||||
_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);
|
||||
},
|
||||
|
@ -813,13 +835,13 @@ editor = {
|
|||
editor._set_max_bounds();
|
||||
|
||||
if (same_url && editor._last_geometry_update_cache_key) {
|
||||
geometry_url += '?update_cache_key='+editor._last_geometry_update_cache_key;
|
||||
geometry_url += '?update_cache_key=' + editor._last_geometry_update_cache_key;
|
||||
}
|
||||
c3nav_api.get(geometry_url)
|
||||
.then(result => {
|
||||
var geometries = [], feature, new_cache = {}, feature_type, feature_id;
|
||||
// geometries cache logic
|
||||
for (var i=0;i<result.length;i++) {
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
feature = result[i];
|
||||
if (Array.isArray(feature)) {
|
||||
if (feature[0] === 'update_cache_key') {
|
||||
|
@ -855,11 +877,11 @@ editor = {
|
|||
}
|
||||
var remove_feature = null;
|
||||
if (editor._editing_id !== null) {
|
||||
for (i=0;i<geometries.length;i++) {
|
||||
for (i = 0; i < geometries.length; i++) {
|
||||
feature = geometries[i];
|
||||
if (feature.properties.original_type !== null && feature.properties.original_type+'-'+String(feature.properties.original_id) === editor._editing_id) {
|
||||
if (feature.properties.original_type !== null && feature.properties.original_type + '-' + String(feature.properties.original_id) === editor._editing_id) {
|
||||
remove_feature = i;
|
||||
} else if (feature.original_geometry !== null && feature.properties.type+'-'+String(feature.properties.id) === editor._editing_id) {
|
||||
} else if (feature.original_geometry !== null && feature.properties.type + '-' + String(feature.properties.id) === editor._editing_id) {
|
||||
feature.geometry = feature.original_geometry;
|
||||
break;
|
||||
}
|
||||
|
@ -869,8 +891,17 @@ editor = {
|
|||
geometries.splice(remove_feature, 1);
|
||||
}
|
||||
if (editor._last_graph_path === null) {
|
||||
geometries = geometries.filter(function(val) { return val.properties.type !== 'graphnode' && val.properties.type !== 'graphedge' })
|
||||
geometries = geometries.filter(function (val) {
|
||||
return val.properties.type !== 'graphnode' && val.properties.type !== 'graphedge'
|
||||
})
|
||||
}
|
||||
|
||||
if (editor.sidebar_extra_data?.activeOverlayId) {
|
||||
geometries = geometries.filter(g => g.properties.type !== 'dataoverlayfeature' || g.properties.overlay === editor.sidebar_extra_data.activeOverlayId);
|
||||
} else {
|
||||
geometries = geometries.filter(g => g.properties.type !== 'dataoverlayfeature');
|
||||
}
|
||||
|
||||
editor._geometries_layer = L.geoJSON(geometries, {
|
||||
style: editor._get_geometry_style,
|
||||
pointToLayer: editor._point_to_layer,
|
||||
|
@ -900,11 +931,11 @@ editor = {
|
|||
} else {
|
||||
defs.innerHTML = '';
|
||||
}
|
||||
for(i=0;i<editor._arrow_colors.length;i++) {
|
||||
for (i = 0; i < editor._arrow_colors.length; i++) {
|
||||
var color = editor._arrow_colors[i];
|
||||
defs = editor.map.options.renderer._container.querySelector('defs');
|
||||
// noinspection HtmlUnknownAttribute
|
||||
defs.insertAdjacentHTML('beforeend', '<marker id="graph-edge-arrow-'+String(i)+'" markerWidth="2" markerHeight="3" refX="3.5" refY="1.5" orient="auto"><path d="M0,0 L2,1.5 L0,3 L0,0" fill="'+color+'"></path></marker>');
|
||||
defs.insertAdjacentHTML('beforeend', '<marker id="graph-edge-arrow-' + String(i) + '" markerWidth="2" markerHeight="3" refX="3.5" refY="1.5" orient="auto"><path d="M0,0 L2,1.5 L0,3 L0,0" fill="' + color + '"></path></marker>');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -916,23 +947,23 @@ editor = {
|
|||
editor.load_geometries(editor._last_geometry_url);
|
||||
}
|
||||
},
|
||||
_weight_for_zoom: function() {
|
||||
return Math.pow(2, editor.map.getZoom())*0.1;
|
||||
_weight_for_zoom: function () {
|
||||
return Math.pow(2, editor.map.getZoom()) * 0.1;
|
||||
},
|
||||
_adjust_line_zoom: function() {
|
||||
var weight = Math.pow(2, editor.map.getZoom())*0.1,
|
||||
_adjust_line_zoom: function () {
|
||||
var weight = Math.pow(2, editor.map.getZoom()) * 0.1,
|
||||
factor = Math.pow(2, editor.map.getZoom());
|
||||
editor._arrow_colors = [];
|
||||
for(var i=0;i<editor._line_geometries.length;i++) {
|
||||
for (var i = 0; i < editor._line_geometries.length; i++) {
|
||||
var layer = editor._line_geometries[i];
|
||||
if (layer.feature.properties.type === 'stair') {
|
||||
layer.setStyle({weight: weight/2});
|
||||
layer.setStyle({weight: weight / 2});
|
||||
} else {
|
||||
layer.setStyle({weight: weight});
|
||||
}
|
||||
if (layer.feature.properties.type === 'graphedge') {
|
||||
var start_pos = 0.1,
|
||||
end_pos = layer.length-0.1,
|
||||
end_pos = layer.length - 0.1,
|
||||
color_index = editor._arrow_colors.indexOf(layer._path.getAttribute('stroke')),
|
||||
other = (editor._graph_edges_to[layer.feature.properties.from_node] !== undefined) ? editor._graph_edges_to[layer.feature.properties.from_node][layer.feature.properties.to_node] : undefined;
|
||||
if (color_index === -1) {
|
||||
|
@ -940,19 +971,19 @@ editor = {
|
|||
editor._arrow_colors.push(layer._path.getAttribute('stroke'));
|
||||
}
|
||||
if (other !== undefined) {
|
||||
start_pos = layer.length/2-0.01;
|
||||
start_pos = layer.length / 2 - 0.01;
|
||||
}
|
||||
if (other === undefined || layer._path.getAttribute('stroke') !== other._path.getAttribute('stroke')) {
|
||||
end_pos = layer.length-0.3;
|
||||
layer._path.setAttribute('marker-end', 'url(#graph-edge-arrow-'+String(color_index)+')');
|
||||
end_pos = layer.length - 0.3;
|
||||
layer._path.setAttribute('marker-end', 'url(#graph-edge-arrow-' + String(color_index) + ')');
|
||||
}
|
||||
layer.setStyle({
|
||||
dashArray: '0 '+String(start_pos*factor)+' '+String((end_pos-start_pos)*factor)+' '+String(layer.length*factor)
|
||||
dashArray: '0 ' + String(start_pos * factor) + ' ' + String((end_pos - start_pos) * factor) + ' ' + String(layer.length * factor)
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
_line_draw_geometry_style: function(style) {
|
||||
_line_draw_geometry_style: function (style) {
|
||||
style.stroke = true;
|
||||
style.color = style.fillColor;
|
||||
style.weight = editor._weight_for_zoom();
|
||||
|
@ -990,6 +1021,13 @@ editor = {
|
|||
if (feature.properties.opacity !== null) {
|
||||
style.fillOpacity = feature.properties.opacity;
|
||||
}
|
||||
|
||||
if (feature.properties.type === 'dataoverlayfeature') {
|
||||
style.stroke = true;
|
||||
style.weight = 3;
|
||||
style.fillOpacity = 0.5;
|
||||
}
|
||||
|
||||
return style
|
||||
},
|
||||
_get_mapitem_type_style: function (mapitem_type) {
|
||||
|
@ -1005,13 +1043,13 @@ editor = {
|
|||
// onEachFeature callback for GeoJSON loader – register all needed events
|
||||
if (feature.geometry.type === 'LineString') {
|
||||
editor._line_geometries.push(layer);
|
||||
layer.length = Math.pow(Math.pow(layer._latlngs[0].lat-layer._latlngs[1].lat, 2)+Math.pow(layer._latlngs[0].lng-layer._latlngs[1].lng, 2), 0.5);
|
||||
layer.length = Math.pow(Math.pow(layer._latlngs[0].lat - layer._latlngs[1].lat, 2) + Math.pow(layer._latlngs[0].lng - layer._latlngs[1].lng, 2), 0.5);
|
||||
}
|
||||
if (feature.properties.type === editor._highlight_type) {
|
||||
var list_elem = $('#sidebar').find('[data-list] tr[data-pk='+String(feature.properties.id)+']');
|
||||
var list_elem = $('#sidebar').find('[data-list] tr[data-pk=' + String(feature.properties.id) + ']');
|
||||
if (list_elem.length === 0) return;
|
||||
var highlight_layer = L.geoJSON(layer.feature, {
|
||||
style: function() {
|
||||
style: function () {
|
||||
return {
|
||||
weight: 3,
|
||||
opacity: 0,
|
||||
|
@ -1027,14 +1065,14 @@ editor = {
|
|||
.on('mouseout', editor._unhover_geometry_layer)
|
||||
.on('click', editor._click_geometry_layer)
|
||||
.on('dblclick', editor._dblclick_geometry_layer);
|
||||
} else if (feature.properties.type+'-'+String(feature.properties.id) === editor._editing_id) {
|
||||
} else if (feature.properties.type + '-' + String(feature.properties.id) === editor._editing_id) {
|
||||
editor._editing_layer = layer;
|
||||
editor._bounds_layer = layer;
|
||||
} else if (feature.properties.bounds === true) {
|
||||
editor._bounds_layer = layer;
|
||||
if (editor._graph_creating) {
|
||||
var space_layer = L.geoJSON(layer.feature, {
|
||||
style: function() {
|
||||
style: function () {
|
||||
return {
|
||||
weight: 0,
|
||||
opacity: 0,
|
||||
|
@ -1047,7 +1085,7 @@ editor = {
|
|||
}
|
||||
} else if (feature.properties.type === 'graphnode' && editor._graph_editing) {
|
||||
var node_layer = L.geoJSON(layer.feature, {
|
||||
style: function() {
|
||||
style: function () {
|
||||
return {
|
||||
weight: 3,
|
||||
opacity: 0,
|
||||
|
@ -1063,7 +1101,7 @@ editor = {
|
|||
.on('click', editor._click_graph_node);
|
||||
} else if (feature.properties.type === 'space' && editor._graph_editing && !editor._graph_creating) {
|
||||
var other_space_layer = L.geoJSON(layer.feature, {
|
||||
style: function() {
|
||||
style: function () {
|
||||
return {
|
||||
weight: 3,
|
||||
opacity: 0,
|
||||
|
@ -1132,11 +1170,10 @@ editor = {
|
|||
e.target.list_elem.find('td:last-child a').click();
|
||||
editor.map.doubleClickZoom.disable();
|
||||
},
|
||||
_highlight_geometry: function(id) {
|
||||
_highlight_geometry: function (id) {
|
||||
// highlight a geometries layer and itemtable row if they both exist
|
||||
var geometry = editor._highlight_geometries[id];
|
||||
if (!geometry) return;
|
||||
if (Object.keys(geometry._bounds).length === 0) return; // ignore geometries with empty bounds
|
||||
geometry.setStyle({
|
||||
color: '#FFFFDD',
|
||||
weight: 3,
|
||||
|
@ -1145,11 +1182,10 @@ editor = {
|
|||
});
|
||||
geometry.list_elem.addClass('highlight');
|
||||
},
|
||||
_unhighlight_geometry: function(id) {
|
||||
_unhighlight_geometry: function (id) {
|
||||
// unhighlight whatever is highlighted currently
|
||||
var geometry = editor._highlight_geometries[id];
|
||||
if (!geometry) return;
|
||||
if (Object.keys(geometry._bounds).length === 0) return; // ignore geometries with empty bounds
|
||||
geometry.setStyle({
|
||||
weight: 3,
|
||||
opacity: 0,
|
||||
|
@ -1159,7 +1195,7 @@ editor = {
|
|||
},
|
||||
|
||||
// graph events
|
||||
_hover_graph_item: function(e) {
|
||||
_hover_graph_item: function (e) {
|
||||
// hover callback for a graph node
|
||||
if (editor._loading_geometry) return;
|
||||
e.target.setStyle({
|
||||
|
@ -1169,7 +1205,7 @@ editor = {
|
|||
fillOpacity: 0
|
||||
});
|
||||
},
|
||||
_unhover_graph_item: function(e) {
|
||||
_unhover_graph_item: function (e) {
|
||||
// unhover callback for a graph node
|
||||
if (editor._loading_geometry) return;
|
||||
e.target.setStyle({
|
||||
|
@ -1178,13 +1214,13 @@ editor = {
|
|||
fillOpacity: 0
|
||||
});
|
||||
},
|
||||
_click_graph_current_space: function(e) {
|
||||
_click_graph_current_space: function (e) {
|
||||
// click callback for a current graph space
|
||||
if (editor._loading_geometry) return;
|
||||
$('#id_clicked_position').val(JSON.stringify(L.marker(e.latlng).toGeoJSON().geometry)).closest('form').submit();
|
||||
editor.map.doubleClickZoom.disable();
|
||||
},
|
||||
_click_graph_node: function(e) {
|
||||
_click_graph_node: function (e) {
|
||||
// click callback for a graph node
|
||||
if (editor._loading_geometry) return;
|
||||
if (editor._active_graph_node === e.target.feature.properties.id) {
|
||||
|
@ -1204,7 +1240,7 @@ editor = {
|
|||
$('#id_clicked_node').val(e.target.feature.properties.id).closest('form').submit();
|
||||
editor.map.doubleClickZoom.disable();
|
||||
},
|
||||
_dblclick_graph_other_space: function(e) {
|
||||
_dblclick_graph_other_space: function (e) {
|
||||
// click callback for an other graph space
|
||||
if (editor._loading_geometry) return;
|
||||
editor._next_zoom = true;
|
||||
|
@ -1213,7 +1249,7 @@ editor = {
|
|||
},
|
||||
|
||||
// edit and create geometries
|
||||
_check_start_editing: function() {
|
||||
_check_start_editing: function () {
|
||||
// called on sidebar load. start editing or creating depending on how the sidebar may require it
|
||||
var sidebarcontent = $('#sidebar').find('.content');
|
||||
|
||||
|
@ -1237,7 +1273,9 @@ editor = {
|
|||
}
|
||||
if (options) {
|
||||
editor._editing_layer = L.geoJSON(JSON.parse(geometry_field.val()), {
|
||||
style: function() { return options; },
|
||||
style: function () {
|
||||
return options;
|
||||
},
|
||||
pointToLayer: editor._point_to_layer,
|
||||
}).getLayers()[0].addTo(editor._geometries_layer);
|
||||
editor._editing_layer.enableEdit();
|
||||
|
@ -1252,21 +1290,43 @@ editor = {
|
|||
options.fillOpacity = 0.5;
|
||||
}
|
||||
form.addClass('creation-lock');
|
||||
var geomtype = form.attr('data-geomtype');
|
||||
const geomtypes = form.attr('data-geomtype').split(',');
|
||||
|
||||
const startGeomEditing = (geomtype) => {
|
||||
editor._creating_type = geomtype;
|
||||
editor._creating = true;
|
||||
if (editor._current_editing_shape) {
|
||||
editor._current_editing_shape.remove();
|
||||
}
|
||||
if (geomtype === 'polygon') {
|
||||
editor.map.editTools.startPolygon(null, options);
|
||||
editor._current_editing_shape = editor.map.editTools.startPolygon(null, options);
|
||||
} else if (geomtype === 'linestring') {
|
||||
options = editor._line_draw_geometry_style(options);
|
||||
editor.map.editTools.startPolyline(null, options);
|
||||
editor._current_editing_shape = editor.map.editTools.startPolyline(null, options);
|
||||
} else if (geomtype === 'point') {
|
||||
editor.map.editTools.startMarker(null, options);
|
||||
editor._current_editing_shape = editor.map.editTools.startMarker(null, options);
|
||||
}
|
||||
editor._creating = true;
|
||||
}
|
||||
|
||||
if (geomtypes.length > 1) {
|
||||
const selector = $('<select id="geomtype-selector"></select>');
|
||||
const geomtypeNames = {
|
||||
polygon: 'Polygon',
|
||||
linestring: 'Line string',
|
||||
point: 'Point'
|
||||
}; // TODO: translations
|
||||
for(const geomtype of geomtypes) {
|
||||
selector.append(`<option value="${geomtype}">${geomtypeNames[geomtype]}</option>`);
|
||||
}
|
||||
|
||||
selector.on('change', e => startGeomEditing(e.target.value));
|
||||
form.prepend(selector);
|
||||
}
|
||||
startGeomEditing(geomtypes[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
_cancel_editing: function() {
|
||||
_cancel_editing: function () {
|
||||
// called on sidebar unload. cancel all editing and creating.
|
||||
if (editor._creating) {
|
||||
editor._creating = false;
|
||||
|
@ -1285,7 +1345,7 @@ editor = {
|
|||
//e.layer.remove();
|
||||
}
|
||||
},
|
||||
_done_creating: function(e) {
|
||||
_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;
|
||||
|
@ -1350,7 +1410,7 @@ editor = {
|
|||
// todo: maybe reset if either is empty?
|
||||
if (!editor._scancollector_data.wifi.length && editor._scancollector_data.ibeacon.length) return editor._scancollector_reset();
|
||||
var $collector = $('#sidebar').find('.scancollector');
|
||||
$collector.removeClass('running').delay(1000).queue(function(n) {
|
||||
$collector.removeClass('running').delay(1000).queue(function (n) {
|
||||
$(this).addClass('done');
|
||||
n();
|
||||
});
|
||||
|
@ -1358,25 +1418,27 @@ editor = {
|
|||
},
|
||||
_scancollector_reset: function () {
|
||||
var $collector = $('#sidebar').find('.scancollector');
|
||||
$collector.removeClass('done').removeClass('running').addClass('empty').find('table tbody').each(function(elem) {elem.innerHTML = "";});
|
||||
$collector.removeClass('done').removeClass('running').addClass('empty').find('table tbody').each(function (elem) {
|
||||
elem.innerHTML = "";
|
||||
});
|
||||
$collector.siblings('[name=data]').val('');
|
||||
$collector.closest('form').addClass('scan-lock');
|
||||
editor._beacon_layer.clearLayers();
|
||||
},
|
||||
_scancollector_wifi_last_max_last: 0,
|
||||
_scancollector_wifi_last_result: 0,
|
||||
_scancollector_wifi_result: function(data) {
|
||||
_scancollector_wifi_result: function (data) {
|
||||
var $collector = $('#sidebar').find('.scancollector.running'),
|
||||
$table = $collector.find('.wifi-table tbody'),
|
||||
item, i, line, apid, color, max_last = 0, now = Date.now(), match;
|
||||
editor._wifi_scan_waits = false;
|
||||
|
||||
if (!data.length) return;
|
||||
if (now-2000 < editor._scancollector_wifi_last_result) return;
|
||||
if (now - 2000 < editor._scancollector_wifi_last_result) return;
|
||||
editor._scancollector_wifi_last_result = now;
|
||||
|
||||
// ignore this scan?
|
||||
for (i=0; i < data.length; i++) {
|
||||
for (i = 0; i < data.length; i++) {
|
||||
item = data[i];
|
||||
if (item.last) {
|
||||
max_last = Math.max(max_last, item.last);
|
||||
|
@ -1386,7 +1448,7 @@ editor = {
|
|||
editor._scancollector_wifi_last_max_last = max_last;
|
||||
|
||||
$table.find('tr').addClass('old');
|
||||
for (i=0; i < data.length; i++) {
|
||||
for (i = 0; i < data.length; i++) {
|
||||
item = data[i];
|
||||
// App version < 4.2.4 use level instead fo rssi
|
||||
if (item.level !== undefined) {
|
||||
|
@ -1400,10 +1462,10 @@ editor = {
|
|||
delete item.rtt;
|
||||
}
|
||||
|
||||
apid = 'ap-'+item.bssid.replace(/:/g, '-');
|
||||
line = $table.find('tr.'+apid);
|
||||
color = Math.max(0, Math.min(50, item.rssi+80));
|
||||
color = 'rgb('+String(250-color*5)+', '+String(color*4)+', 0)';
|
||||
apid = 'ap-' + item.bssid.replace(/:/g, '-');
|
||||
line = $table.find('tr.' + apid);
|
||||
color = Math.max(0, Math.min(50, item.rssi + 80));
|
||||
color = 'rgb(' + String(250 - color * 5) + ', ' + String(color * 4) + ', 0)';
|
||||
if (line.length) {
|
||||
line.removeClass('old').find(':last-child').text(item.rssi).css('color', color);
|
||||
} else {
|
||||
|
@ -1417,7 +1479,7 @@ editor = {
|
|||
}
|
||||
shortened_ssid = item.ssid;
|
||||
if (shortened_ssid.length > 20) {
|
||||
shortened_ssid = shortened_ssid.slice(0, 20)+'…';
|
||||
shortened_ssid = shortened_ssid.slice(0, 20) + '…';
|
||||
}
|
||||
line = $('<tr>').addClass(apid);
|
||||
line.append($('<td>').text(item.bssid));
|
||||
|
@ -1431,7 +1493,7 @@ editor = {
|
|||
$collector.find('.wifi-count').text(editor._scancollector_data.wifi.length);
|
||||
$collector.siblings('[name=data]').val(JSON.stringify(editor._scancollector_data));
|
||||
},
|
||||
_scancollector_ibeacon_result: function(data) {
|
||||
_scancollector_ibeacon_result: function (data) {
|
||||
var $collector = $('#sidebar').find('.scancollector.running'),
|
||||
$table = $collector.find('.ibeacon-table tbody'),
|
||||
item, i, line, beaconid, color = Date.now(), match;
|
||||
|
@ -1439,14 +1501,14 @@ editor = {
|
|||
if (!data.length) return;
|
||||
|
||||
$table.find('tr').addClass('old');
|
||||
for (i=0; i < data.length; i++) {
|
||||
for (i = 0; i < data.length; i++) {
|
||||
item = data[i];
|
||||
beaconid = 'beacon-'+item.uuid+'-'+item.major+'-'+item.minor;
|
||||
line = $table.find('tr.'+beaconid);
|
||||
beaconid = 'beacon-' + item.uuid + '-' + item.major + '-' + item.minor;
|
||||
line = $table.find('tr.' + beaconid);
|
||||
color = Math.max(0, Math.min(50, item.distance));
|
||||
color = 'rgb('+String(color*5)+', '+String(200-color*4)+', 0)';
|
||||
color = 'rgb(' + String(color * 5) + ', ' + String(200 - color * 4) + ', 0)';
|
||||
if (line.length) {
|
||||
line.removeClass('old').find(':last-child').text(Math.round(item.distance*100)/100).css('color', color);
|
||||
line.removeClass('old').find(':last-child').text(Math.round(item.distance * 100) / 100).css('color', color);
|
||||
} else {
|
||||
match = editor._scancollector_lookup.ibeacons?.[item.uuid]?.[item.major]?.[item.minor];
|
||||
if (match && match.point) {
|
||||
|
@ -1460,7 +1522,7 @@ editor = {
|
|||
line.append($('<td>').text(item.major));
|
||||
line.append($('<td>').text(item.minor));
|
||||
line.append($('<td>').text(match ? match.name : ''));
|
||||
line.append($('<td>').text(Math.round(item.distance*100)/100).css('color', color));
|
||||
line.append($('<td>').text(Math.round(item.distance * 100) / 100).css('color', color));
|
||||
$table.append(line);
|
||||
}
|
||||
}
|
||||
|
@ -1469,7 +1531,7 @@ editor = {
|
|||
$collector.siblings('[name=data]').val(JSON.stringify(editor._scancollector_data));
|
||||
},
|
||||
_wifi_scan_waits: false,
|
||||
_scancollector_wifi_scan_perhaps: function() {
|
||||
_scancollector_wifi_scan_perhaps: function () {
|
||||
if (!editor._wifi_scan_waits && $('#sidebar').find('.scancollector.running').length) {
|
||||
editor._wifi_scan_waits = true;
|
||||
mobileclient.scanNow();
|
||||
|
@ -1492,7 +1554,7 @@ LevelControl = L.Control.extend({
|
|||
},
|
||||
|
||||
onAdd: function () {
|
||||
this._container = L.DomUtil.create('div', 'leaflet-control-levels leaflet-bar '+this.options.addClasses);
|
||||
this._container = L.DomUtil.create('div', 'leaflet-control-levels leaflet-bar ' + this.options.addClasses);
|
||||
this._levelButtons = [];
|
||||
//noinspection JSUnusedGlobalSymbols
|
||||
this.current_level_id = null;
|
||||
|
@ -1533,7 +1595,7 @@ LevelControl = L.Control.extend({
|
|||
return link;
|
||||
},
|
||||
|
||||
clearLevels: function() {
|
||||
clearLevels: function () {
|
||||
this.current_level_id = null;
|
||||
this.level_ids = [];
|
||||
for (var i = 0; i < this._levelButtons.length; i++) {
|
||||
|
@ -1601,7 +1663,7 @@ OverlayControl = L.Control.extend({
|
|||
|
||||
onAdd: function () {
|
||||
this._initialActiveOverlays = JSON.parse(localStorage.getItem('c3nav.editor.overlays.active-overlays') ?? '[]');
|
||||
this._initialCollapsedGroups = JSON.parse(localStorage.getItem('c3nav.editor.overlays.collapsedGroups') ?? '[]');
|
||||
this._initialCollapsedGroups = JSON.parse(localStorage.getItem('c3nav.editor.overlays.collapsed-groups') ?? '[]');
|
||||
const pinned = JSON.parse(localStorage.getItem('c3nav.editor.overlays.pinned') ?? 'false');
|
||||
|
||||
this._container = L.DomUtil.create('div', 'leaflet-control-overlays ' + this.options.addClasses);
|
||||
|
@ -1677,7 +1739,7 @@ OverlayControl = L.Control.extend({
|
|||
this.render();
|
||||
},
|
||||
|
||||
updateOverlay: function(id) {
|
||||
updateOverlay: function (id) {
|
||||
const overlay = this._overlays[id];
|
||||
if (overlay.visible) {
|
||||
overlay.layer.addTo(this._map);
|
||||
|
@ -1732,7 +1794,7 @@ OverlayControl = L.Control.extend({
|
|||
return this;
|
||||
},
|
||||
|
||||
toggleGroup: function(name) {
|
||||
toggleGroup: function (name) {
|
||||
const group = this._groups[name];
|
||||
group.expanded = !group.expanded;
|
||||
group.el.classList.toggle('expanded', group.expanded);
|
||||
|
@ -1740,7 +1802,7 @@ OverlayControl = L.Control.extend({
|
|||
localStorage.setItem('c3nav.editor.overlays.collapsed-groups', JSON.stringify(collapsedGroups));
|
||||
},
|
||||
|
||||
togglePinned: function() {
|
||||
togglePinned: function () {
|
||||
this._pinned = !this._pinned;
|
||||
if (this._pinned) {
|
||||
this._expanded = true;
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
|
||||
{% include 'editor/fragment_levels.html' %}
|
||||
|
||||
{% if extra_json_data %}
|
||||
{{ extra_json_data|json_script:"sidebar-extra-data" }}
|
||||
{% endif %}
|
||||
|
||||
<h3>
|
||||
{% if new %}
|
||||
{% blocktrans %}New {{ model_title }}{% endblocktrans %}
|
||||
|
|
|
@ -12,4 +12,7 @@
|
|||
{% trans 'Graph' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if overlays_url %}
|
||||
<a href="{{ overlays_url }}" class="list-group-item">{% trans 'Overlays' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
</p>
|
||||
|
||||
{% url 'editor.levels.graph' level=level.pk as graph_url %}
|
||||
{% include 'editor/fragment_child_models.html' with graph_url=graph_url %}
|
||||
{% url 'editor.levels.overlays' level=level.pk as overlays_url %}
|
||||
{% include 'editor/fragment_child_models.html' with graph_url=graph_url overlays_url=overlays_url %}
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
|
45
src/c3nav/editor/templates/editor/overlay_features.html
Normal file
45
src/c3nav/editor/templates/editor/overlay_features.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% include 'editor/fragment_levels.html' %}
|
||||
|
||||
{% if extra_json_data %}
|
||||
{{ extra_json_data|json_script:"sidebar-extra-data" }}
|
||||
{% endif %}
|
||||
|
||||
<h3>
|
||||
{% blocktrans %}Overlay "{{ title }}"{% endblocktrans %}
|
||||
{% with level.title as level_title %}
|
||||
<small>{% blocktrans %}on level {{ level_title }}{% endblocktrans %}</small>
|
||||
{% endwith %}
|
||||
|
||||
</h3>
|
||||
{% bootstrap_messages %}
|
||||
{% if can_create %}
|
||||
<a class="btn btn-default btn-xs" accesskey="n" href="{{ create_url }}">
|
||||
<i class="glyphicon glyphicon-plus"></i> {% blocktrans %}New feature{% endblocktrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% trans 'Edit' as edit_caption %}
|
||||
|
||||
<table class="table table-condensed itemtable" data-nozoom data-list="dataoverlayfeature" data-overlay-id="{{ overlay_id }}">
|
||||
<tbody>
|
||||
{% for feature in features %}
|
||||
{% if forloop.counter0|divisibleby:10 %}
|
||||
<tr>
|
||||
<td><a href="{{ back_url }}" data-no-next-zoom>« {{ back_title }}</a></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr data-pk="{{ feature.pk }}">
|
||||
<td>{{ feature.title }}</td>
|
||||
<td><a href="{{ feature.edit_url }}">{{ edit_caption }}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td><a href="{{ back_url }}" data-no-next-zoom>« {{ back_title }}</a></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
17
src/c3nav/editor/templates/editor/overlays.html
Normal file
17
src/c3nav/editor/templates/editor/overlays.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% include 'editor/fragment_levels.html' %}
|
||||
|
||||
<h3>{% trans 'Data Overlays' %}</h3>
|
||||
{% bootstrap_messages %}
|
||||
<p>
|
||||
<a href="{% url 'editor.levels.detail' pk=level.pk %}">« {% trans 'back to level' %}</a>
|
||||
</p>
|
||||
|
||||
<div class="list-group">
|
||||
{% for overlay in overlays %}
|
||||
<a href="{% url 'editor.levels.overlay' level=level.pk pk=overlay.pk %}" class="list-group-item">
|
||||
{{ overlay.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -5,6 +5,7 @@ from c3nav.editor.converters import EditPkConverter
|
|||
from c3nav.editor.views.account import change_password_view, login_view, logout_view, register_view
|
||||
from c3nav.editor.views.changes import changeset_detail, changeset_edit, changeset_redirect
|
||||
from c3nav.editor.views.edit import edit, graph_edit, level_detail, list_objects, main_index, sourceimage, space_detail
|
||||
from c3nav.editor.views.overlays import overlays_list, overlay_features, overlay_feature_edit
|
||||
from c3nav.editor.views.users import user_detail, user_redirect
|
||||
|
||||
register_converter(EditPkConverter, 'editpk')
|
||||
|
@ -42,6 +43,10 @@ urlpatterns = [
|
|||
name='editor.levels_on_top.create'),
|
||||
path('levels/<editpk:level>/graph/', graph_edit, name='editor.levels.graph'),
|
||||
path('spaces/<editpk:space>/graph/', graph_edit, name='editor.spaces.graph'),
|
||||
path('levels/<editpk:level>/overlays/', overlays_list, name='editor.levels.overlays'),
|
||||
path('levels/<editpk:level>/overlays/<editpk:pk>/', overlay_features, name='editor.levels.overlay'),
|
||||
path('levels/<editpk:level>/overlays/<editpk:overlay>/create', overlay_feature_edit, name='editor.levels.overlay.create'),
|
||||
path('levels/<editpk:level>/overlays/<editpk:overlay>/features/<editpk:pk>', overlay_feature_edit, name='editor.levels.overlay.edit'),
|
||||
path('changeset/', changeset_redirect, name='editor.changesets.current'),
|
||||
path('changesets/<editpk:pk>/', changeset_detail, name='editor.changesets.detail'),
|
||||
path('changesets/<editpk:pk>/edit', changeset_edit, name='editor.changesets.edit'),
|
||||
|
@ -66,6 +71,7 @@ urlpatterns.extend(add_editor_urls('AccessRestrictionGroup'))
|
|||
urlpatterns.extend(add_editor_urls('Source'))
|
||||
urlpatterns.extend(add_editor_urls('LabelSettings'))
|
||||
urlpatterns.extend(add_editor_urls('Theme'))
|
||||
urlpatterns.extend(add_editor_urls('DataOverlay'))
|
||||
urlpatterns.extend(add_editor_urls('Building', 'Level'))
|
||||
urlpatterns.extend(add_editor_urls('Space', 'Level', explicit_edit=True))
|
||||
urlpatterns.extend(add_editor_urls('Door', 'Level'))
|
||||
|
|
|
@ -60,6 +60,7 @@ def main_index(request):
|
|||
child_model(request, 'LabelSettings'),
|
||||
child_model(request, 'Source'),
|
||||
child_model(request, 'Theme'),
|
||||
child_model(request, 'DataOverlay'),
|
||||
],
|
||||
}, fields=('can_create_level', 'child_models'))
|
||||
|
||||
|
|
272
src/c3nav/editor/views/overlays.py
Normal file
272
src/c3nav/editor/views/overlays.py
Normal file
|
@ -0,0 +1,272 @@
|
|||
from c3nav.editor.views.base import (APIHybridError, APIHybridFormTemplateResponse,
|
||||
APIHybridMessageRedirectResponse, APIHybridTemplateContextResponse,
|
||||
editor_etag_func, sidebar_view)
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.decorators.http import etag
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.contrib import messages
|
||||
from django.db import IntegrityError
|
||||
|
||||
from c3nav.editor.utils import DefaultEditUtils, LevelChildEditUtils
|
||||
from c3nav.editor.views.edit import get_changeset_exceeded
|
||||
|
||||
|
||||
@etag(editor_etag_func)
|
||||
@sidebar_view(api_hybrid=True)
|
||||
def overlays_list(request, level):
|
||||
Level = request.changeset.wrap_model('Level')
|
||||
DataOverlay = request.changeset.wrap_model('DataOverlay')
|
||||
|
||||
queryset = DataOverlay.objects.all().order_by('id')
|
||||
if hasattr(DataOverlay, 'q_for_request'):
|
||||
queryset = queryset.filter(DataOverlay.q_for_request(request))
|
||||
|
||||
level = get_object_or_404(Level.objects.filter(Level.q_for_request(request)), pk=level)
|
||||
edit_utils = LevelChildEditUtils(level, request)
|
||||
|
||||
ctx = {
|
||||
'levels': Level.objects.filter(Level.q_for_request(request), on_top_of__isnull=True),
|
||||
'level': level,
|
||||
'level_url': 'editor.levels.overlays',
|
||||
'geometry_url': edit_utils.geometry_url,
|
||||
'overlays': queryset,
|
||||
}
|
||||
|
||||
return APIHybridTemplateContextResponse('editor/overlays.html', ctx, fields=('overlays',))
|
||||
|
||||
@etag(editor_etag_func)
|
||||
@sidebar_view(api_hybrid=True)
|
||||
def overlay_features(request, level, pk):
|
||||
Level = request.changeset.wrap_model('Level')
|
||||
DataOverlay = request.changeset.wrap_model('DataOverlay')
|
||||
DataOverlayFeature = request.changeset.wrap_model('DataOverlayFeature')
|
||||
|
||||
ctx = {
|
||||
'path': request.path,
|
||||
'overlay_id': pk,
|
||||
}
|
||||
|
||||
queryset = DataOverlayFeature.objects.filter(level_id=level, overlay_id=pk).order_by('id')
|
||||
reverse_kwargs = {}
|
||||
|
||||
add_cols = []
|
||||
|
||||
reverse_kwargs['level'] = level
|
||||
reverse_kwargs['overlay'] = pk
|
||||
level = get_object_or_404(Level.objects.filter(Level.q_for_request(request)), pk=level)
|
||||
overlay = get_object_or_404(DataOverlay.objects.filter(DataOverlay.q_for_request(request)), pk=pk)
|
||||
edit_utils = LevelChildEditUtils(level, request)
|
||||
ctx.update({
|
||||
'title': overlay.title,
|
||||
'back_url': reverse('editor.levels.overlays', kwargs={'level': level.pk}),
|
||||
'back_title': _('back to overlays'),
|
||||
'levels': Level.objects.filter(Level.q_for_request(request), on_top_of__isnull=True),
|
||||
'level': level,
|
||||
|
||||
# TODO: this makes the level switcher always link to the overview of all overlays, rather than the current overlay
|
||||
# unclear how to make it possible to switch to the correct overlay
|
||||
'level_url': 'editor.levels.overlays',
|
||||
})
|
||||
|
||||
for obj in queryset:
|
||||
reverse_kwargs['pk'] = obj.pk
|
||||
obj.edit_url = reverse('editor.levels.overlay.edit', kwargs=reverse_kwargs)
|
||||
obj.add_cols = tuple(getattr(obj, col) for col in add_cols)
|
||||
reverse_kwargs.pop('pk', None)
|
||||
|
||||
|
||||
ctx.update({
|
||||
'can_create': True,
|
||||
'geometry_url': edit_utils.geometry_url,
|
||||
'add_cols': add_cols,
|
||||
'create_url': reverse('editor.levels.overlay.create', kwargs={'level': level.pk, 'overlay': overlay.pk}),
|
||||
'features': queryset,
|
||||
'extra_json_data': {
|
||||
'activeOverlayId': overlay.pk
|
||||
},
|
||||
})
|
||||
|
||||
return APIHybridTemplateContextResponse('editor/overlay_features.html', ctx,
|
||||
fields=('can_create', 'create_url', 'objects'))
|
||||
|
||||
@etag(editor_etag_func)
|
||||
@sidebar_view(api_hybrid=True)
|
||||
def overlay_feature_edit(request, level, overlay, pk=None):
|
||||
changeset_exceeded = get_changeset_exceeded(request)
|
||||
model_changes = {}
|
||||
if changeset_exceeded:
|
||||
model_changes = request.changeset.get_changed_objects_by_model('DataOverlayFeature')
|
||||
|
||||
Level = request.changeset.wrap_model('Level')
|
||||
DataOverlay = request.changeset.wrap_model('DataOverlay')
|
||||
DataOverlayFeature = request.changeset.wrap_model('DataOverlayFeature')
|
||||
|
||||
can_edit_changeset = request.changeset.can_edit(request)
|
||||
|
||||
obj = None
|
||||
edit_utils = DefaultEditUtils(request)
|
||||
if pk is not None:
|
||||
# Edit existing map item
|
||||
kwargs = {'pk': pk}
|
||||
qs = DataOverlayFeature.objects.all()
|
||||
if hasattr(DataOverlayFeature, 'q_for_request'):
|
||||
qs = qs.filter(DataOverlayFeature.q_for_request(request))
|
||||
|
||||
kwargs.update({'level__pk': level})
|
||||
qs = qs.select_related('level')
|
||||
utils_cls = LevelChildEditUtils
|
||||
|
||||
obj = get_object_or_404(qs, **kwargs)
|
||||
level = obj.level
|
||||
overlay = obj.overlay
|
||||
edit_utils = utils_cls.from_obj(obj, request)
|
||||
else:
|
||||
level = get_object_or_404(Level.objects.filter(Level.q_for_request(request)), pk=level)
|
||||
overlay = get_object_or_404(DataOverlay.objects.filter(DataOverlay.q_for_request(request)), pk=overlay)
|
||||
edit_utils = LevelChildEditUtils(level, request)
|
||||
|
||||
new = obj is None
|
||||
|
||||
if new and not edit_utils.can_create:
|
||||
raise PermissionDenied
|
||||
|
||||
geometry_url = edit_utils.geometry_url
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
ctx = {
|
||||
'path': request.path,
|
||||
'pk': pk,
|
||||
'model_name': DataOverlayFeature.__name__.lower(),
|
||||
'model_title': DataOverlayFeature._meta.verbose_name,
|
||||
'can_edit': can_edit_changeset,
|
||||
'new': new,
|
||||
'title': obj.title if obj else None,
|
||||
'geometry_url': geometry_url,
|
||||
'geomtype': 'polygon,linestring,point',
|
||||
}
|
||||
|
||||
space_id = None
|
||||
|
||||
ctx.update({
|
||||
'level': level,
|
||||
'back_url': reverse('editor.levels.overlay', kwargs={'level': level.pk, 'pk': overlay.pk}),
|
||||
})
|
||||
|
||||
nosave = False
|
||||
if changeset_exceeded:
|
||||
if new:
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error', message=_('You can not create new objects because your changeset is full.'),
|
||||
redirect_to=ctx['back_url'], status_code=409,
|
||||
)
|
||||
elif obj.pk not in model_changes:
|
||||
messages.warning(request, _('You can not edit this object because your changeset is full.'))
|
||||
nosave = True
|
||||
|
||||
ctx.update({
|
||||
'nosave': nosave
|
||||
})
|
||||
|
||||
if new:
|
||||
ctx.update({
|
||||
'nozoom': True
|
||||
})
|
||||
|
||||
error = None
|
||||
delete = getattr(request, 'is_delete', None)
|
||||
|
||||
if request.method == 'POST' or (not new and delete):
|
||||
if nosave:
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error', message=_('You can not edit this object because your changeset is full.'),
|
||||
redirect_to=request.path, status_code=409,
|
||||
)
|
||||
|
||||
if not can_edit_changeset:
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error', message=_('You can not edit changes on this changeset.'),
|
||||
redirect_to=request.path, status_code=403,
|
||||
)
|
||||
|
||||
if not new and ((request.POST.get('delete') == '1' and delete is not False) or delete):
|
||||
# Delete this mapitem!
|
||||
try:
|
||||
if not request.changeset.get_changed_object(obj).can_delete():
|
||||
raise PermissionError
|
||||
except (ObjectDoesNotExist, PermissionError):
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error',
|
||||
message=_('You can not delete this object because other objects still depend on it.'),
|
||||
redirect_to=request.path, status_code=409,
|
||||
)
|
||||
|
||||
if request.POST.get('delete_confirm') == '1' or delete:
|
||||
with request.changeset.lock_to_edit(request) as changeset:
|
||||
if changeset.can_edit(request):
|
||||
obj.delete()
|
||||
else:
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='error',
|
||||
message=_('You can not edit changes on this changeset.'),
|
||||
redirect_to=request.path, status_code=403,
|
||||
)
|
||||
|
||||
redirect_to = ctx['back_url']
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='success',
|
||||
message=_('Object was successfully deleted.'),
|
||||
redirect_to=redirect_to
|
||||
)
|
||||
ctx['obj_title'] = obj.title
|
||||
return APIHybridTemplateContextResponse('editor/delete.html', ctx, fields=())
|
||||
|
||||
json_body = getattr(request, 'json_body', None)
|
||||
data = json_body if json_body is not None else request.POST
|
||||
form = DataOverlayFeature.EditorForm(instance=DataOverlayFeature() if new else obj, data=data, is_json=json_body is not None,
|
||||
request=request, space_id=space_id,
|
||||
geometry_editable=edit_utils.can_access_child_base_mapdata)
|
||||
if form.is_valid():
|
||||
# Update/create objects
|
||||
obj = form.save(commit=False)
|
||||
|
||||
obj.level = level
|
||||
obj.overlay = overlay
|
||||
|
||||
with request.changeset.lock_to_edit(request) as changeset:
|
||||
if changeset.can_edit(request):
|
||||
try:
|
||||
obj.save()
|
||||
except IntegrityError as e:
|
||||
error = APIHybridError(status_code=400, message=_('Duplicate entry.'))
|
||||
else:
|
||||
if form.redirect_slugs is not None:
|
||||
for slug in form.add_redirect_slugs:
|
||||
obj.redirects.create(slug=slug)
|
||||
|
||||
for slug in form.remove_redirect_slugs:
|
||||
obj.redirects.filter(slug=slug).delete()
|
||||
|
||||
form.save_m2m()
|
||||
return APIHybridMessageRedirectResponse(
|
||||
level='success',
|
||||
message=_('Object was successfully saved.'),
|
||||
redirect_to=ctx['back_url']
|
||||
)
|
||||
else:
|
||||
error = APIHybridError(status_code=403, message=_('You can not edit changes on this changeset.'))
|
||||
|
||||
else:
|
||||
form = DataOverlayFeature.EditorForm(instance=obj, request=request, space_id=space_id,
|
||||
geometry_editable=edit_utils.can_access_child_base_mapdata)
|
||||
|
||||
ctx.update({
|
||||
'form': form,
|
||||
'extra_json_data': {
|
||||
'activeOverlayId': overlay.pk
|
||||
}
|
||||
})
|
||||
|
||||
return APIHybridFormTemplateResponse('editor/edit.html', ctx, form=form, error=error)
|
||||
|
|
@ -8,7 +8,7 @@ from c3nav.api.auth import auth_responses, validate_responses
|
|||
from c3nav.api.exceptions import API404
|
||||
from c3nav.mapdata.api.base import api_etag, optimize_query
|
||||
from c3nav.mapdata.models import (Area, Building, Door, Hole, Level, LocationGroup, LocationGroupCategory, Source,
|
||||
Space, Stair)
|
||||
Space, Stair, DataOverlay, DataOverlayFeature)
|
||||
from c3nav.mapdata.models.access import AccessRestriction, AccessRestrictionGroup
|
||||
from c3nav.mapdata.models.geometry.space import (POI, Column, CrossDescription, LeaveDescription, LineObstacle,
|
||||
Obstacle, Ramp)
|
||||
|
@ -20,7 +20,8 @@ from c3nav.mapdata.schemas.models import (AccessRestrictionGroupSchema, AccessRe
|
|||
BuildingSchema, ColumnSchema, CrossDescriptionSchema, DoorSchema,
|
||||
DynamicLocationSchema, HoleSchema, LeaveDescriptionSchema, LevelSchema,
|
||||
LineObstacleSchema, LocationGroupCategorySchema, LocationGroupSchema,
|
||||
ObstacleSchema, POISchema, RampSchema, SourceSchema, SpaceSchema, StairSchema)
|
||||
ObstacleSchema, POISchema, RampSchema, SourceSchema, SpaceSchema, StairSchema,
|
||||
DataOverlaySchema, DataOverlayFeatureSchema)
|
||||
|
||||
mapdata_api_router = APIRouter(tags=["mapdata"])
|
||||
|
||||
|
@ -487,3 +488,33 @@ def dynamiclocation_list(request):
|
|||
@api_etag()
|
||||
def dynamiclocation_by_id(request, dynamiclocation_id: int):
|
||||
return mapdata_retrieve_endpoint(request, DynamicLocation, pk=dynamiclocation_id)
|
||||
|
||||
|
||||
"""
|
||||
Data overlays
|
||||
"""
|
||||
|
||||
|
||||
@mapdata_api_router.get('/overlays/', summary="data overlay list",
|
||||
tags=["mapdata-root"], description=schema_description(DynamicLocationSchema),
|
||||
response={200: list[DataOverlaySchema], **auth_responses})
|
||||
@api_etag()
|
||||
def dataoverlay_list(request):
|
||||
return mapdata_list_endpoint(request, model=DataOverlay)
|
||||
|
||||
|
||||
@mapdata_api_router.get('/overlays/{overlay_id}/', summary="features for overlay by overlay ID",
|
||||
tags=["mapdata-root"], description=schema_description(DynamicLocationSchema),
|
||||
response={200: list[DataOverlayFeatureSchema], **API404.dict(), **auth_responses})
|
||||
# @api_etag()
|
||||
def dataoverlay_by_id(request, overlay_id: int):
|
||||
qs = optimize_query(
|
||||
DataOverlayFeature.qs_for_request(request)
|
||||
)
|
||||
|
||||
qs = qs.filter(overlay_id=overlay_id)
|
||||
|
||||
# order_by
|
||||
qs = qs.order_by('pk')
|
||||
|
||||
return qs
|
||||
|
|
|
@ -109,10 +109,10 @@ class GeometryField(models.JSONField):
|
|||
'multipolygon': (Polygon, MultiPolygon),
|
||||
'linestring': (LineString, ),
|
||||
'point': (Point, )
|
||||
}[self.geomtype]
|
||||
}.get(self.geomtype, None)
|
||||
|
||||
def _validate_geomtype(self, value, exception: typing.Type[Exception] = ValidationError):
|
||||
if not isinstance(value, self.classes):
|
||||
if self.classes is not None and not isinstance(value, self.classes):
|
||||
# if you get this error with wrappedgeometry, looked into wrapped_geom
|
||||
raise TypeError('Expected %s instance, got %s, %s instead.' % (
|
||||
' or '.join(c.__name__ for c in self.classes),
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# Generated by Django 5.0.8 on 2024-11-21 10:43
|
||||
|
||||
import c3nav.mapdata.fields
|
||||
import django.core.serializers.json
|
||||
import django.db.models.deletion
|
||||
import django_pydantic_field.compat.django
|
||||
import django_pydantic_field.fields
|
||||
import types
|
||||
import typing
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mapdata', '0110_theme_icon_path_theme_leaflet_marker_config'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DataOverlay',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', c3nav.mapdata.fields.I18nField(blank=True, fallback_any=True, fallback_value='{model} {pk}', plural_name='titles', verbose_name='Title')),
|
||||
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||
('stroke_color', models.TextField(blank=True, null=True, verbose_name='default stroke color')),
|
||||
('stroke_width', models.FloatField(blank=True, null=True, verbose_name='default stroke width')),
|
||||
('fill_color', models.TextField(blank=True, null=True, verbose_name='default fill color')),
|
||||
('pull_url', models.URLField(blank=True, null=True, verbose_name='pull URL')),
|
||||
('pull_headers', django_pydantic_field.fields.PydanticSchemaField(config=None, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True, schema=django_pydantic_field.compat.django.GenericContainer(typing.Union, (django_pydantic_field.compat.django.GenericContainer(dict, (str, str)), types.NoneType)), verbose_name='headers for pull http request (JSON object)')),
|
||||
('pull_interval', models.DurationField(blank=True, null=True, verbose_name='pull interval')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Data Overlay',
|
||||
'verbose_name_plural': 'Data Overlays',
|
||||
'default_related_name': 'data_overlays',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DataOverlayFeature',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', c3nav.mapdata.fields.I18nField(blank=True, fallback_any=True, fallback_value='{model} {pk}', plural_name='titles', verbose_name='Title')),
|
||||
('import_tag', models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag')),
|
||||
('geometry', c3nav.mapdata.fields.GeometryField(default=None, help_text=None)),
|
||||
('external_url', models.URLField(blank=True, null=True, verbose_name='external URL')),
|
||||
('stroke_color', models.CharField(blank=True, max_length=255, null=True, verbose_name='stroke color')),
|
||||
('stroke_width', models.FloatField(blank=True, null=True, verbose_name='stroke width')),
|
||||
('fill_color', models.CharField(blank=True, max_length=255, null=True, verbose_name='fill color')),
|
||||
('show_label', models.BooleanField(default=False, verbose_name='show label')),
|
||||
('show_geometry', models.BooleanField(default=True, verbose_name='show geometry')),
|
||||
('interactive', models.BooleanField(default=True, help_text='disable to make this feature click-through', verbose_name='interactive')),
|
||||
('point_icon', models.CharField(blank=True, help_text='use this material icon to display points, instead of drawing a small circle (only makes sense if the geometry is a point)', max_length=255, null=True, verbose_name='point icon')),
|
||||
('extra_data', django_pydantic_field.fields.PydanticSchemaField(blank=True, config=None, default=None, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True, schema=django_pydantic_field.compat.django.GenericContainer(typing.Union, (django_pydantic_field.compat.django.GenericContainer(dict, (str, str)), types.NoneType)), verbose_name='extra data (JSON object)')),
|
||||
('level', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='data_overlay_features', to='mapdata.level', verbose_name='level')),
|
||||
('overlay', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='mapdata.dataoverlay', verbose_name='Overlay')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -8,3 +8,4 @@ from c3nav.mapdata.models.locations import Location, LocationSlug, LocationGroup
|
|||
from c3nav.mapdata.models.source import Source # noqa
|
||||
from c3nav.mapdata.models.graph import GraphNode, WayType, GraphEdge # noqa
|
||||
from c3nav.mapdata.models.theme import Theme # noqa
|
||||
from c3nav.mapdata.models.overlay import DataOverlay, DataOverlayFeature # noqa
|
84
src/c3nav/mapdata/models/overlay.py
Normal file
84
src/c3nav/mapdata/models/overlay.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
from typing import Optional
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_pydantic_field import SchemaField
|
||||
|
||||
from c3nav.mapdata.fields import GeometryField
|
||||
from c3nav.mapdata.models.base import TitledMixin
|
||||
from c3nav.mapdata.models.geometry.base import GeometryMixin
|
||||
from c3nav.mapdata.utils.geometry import smart_mapping
|
||||
from c3nav.mapdata.utils.json import format_geojson
|
||||
|
||||
|
||||
class DataOverlay(TitledMixin, models.Model):
|
||||
description = models.TextField(blank=True, verbose_name=_('Description'))
|
||||
stroke_color = models.TextField(blank=True, null=True, verbose_name=_('default stroke color'))
|
||||
stroke_width = models.FloatField(blank=True, null=True, verbose_name=_('default stroke width'))
|
||||
fill_color = models.TextField(blank=True, null=True, verbose_name=_('default fill color'))
|
||||
pull_url = models.URLField(blank=True, null=True, verbose_name=_('pull URL'))
|
||||
pull_headers: dict[str, str] = SchemaField(schema=dict[str, str], null=True,
|
||||
verbose_name=_('headers for pull http request (JSON object)'))
|
||||
pull_interval = models.DurationField(blank=True, null=True, verbose_name=_('pull interval'))
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Data Overlay')
|
||||
verbose_name_plural = _('Data Overlays')
|
||||
default_related_name = 'data_overlays'
|
||||
|
||||
class DataOverlayFeature(TitledMixin, GeometryMixin, models.Model):
|
||||
overlay = models.ForeignKey('mapdata.DataOverlay', on_delete=models.CASCADE, verbose_name=_('Overlay'), related_name='features')
|
||||
geometry = GeometryField()
|
||||
level = models.ForeignKey('mapdata.Level', on_delete=models.CASCADE, verbose_name=_('level'), related_name='data_overlay_features')
|
||||
external_url = models.URLField(blank=True, null=True, verbose_name=_('external URL'))
|
||||
stroke_color = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('stroke color'))
|
||||
stroke_width = models.FloatField(blank=True, null=True, verbose_name=_('stroke width'))
|
||||
fill_color = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('fill color'))
|
||||
show_label = models.BooleanField(default=False, verbose_name=_('show label'))
|
||||
show_geometry = models.BooleanField(default=True, verbose_name=_('show geometry'))
|
||||
interactive = models.BooleanField(default=True, verbose_name=_('interactive'),
|
||||
help_text=_('disable to make this feature click-through'))
|
||||
point_icon = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('point icon'),
|
||||
help_text=_(
|
||||
'use this material icon to display points, instead of drawing a small circle (only makes sense if the geometry is a point)'))
|
||||
extra_data: Optional[dict[str, str]] = SchemaField(schema=dict[str, str], blank=True, null=True, default=None,
|
||||
verbose_name=_('extra data (JSON object)'))
|
||||
|
||||
|
||||
def to_geojson(self, instance=None) -> dict:
|
||||
result = {
|
||||
'type': 'Feature',
|
||||
'properties': {
|
||||
'type': 'dataoverlayfeature',
|
||||
'id': self.id,
|
||||
'level': self.level_id,
|
||||
'overlay': self.overlay_id,
|
||||
},
|
||||
'geometry': format_geojson(smart_mapping(self.geometry), rounded=False),
|
||||
}
|
||||
original_geometry = getattr(self, 'original_geometry', None)
|
||||
if original_geometry:
|
||||
result['original_geometry'] = format_geojson(smart_mapping(original_geometry), rounded=False)
|
||||
return result
|
||||
|
||||
def get_geojson_key(self):
|
||||
return 'dataoverlayfeature', self.id
|
||||
|
||||
|
||||
def _serialize(self, **kwargs):
|
||||
result = super()._serialize(**kwargs)
|
||||
result.update({
|
||||
'level_id': self.level_id,
|
||||
'stroke_color': self.stroke_color,
|
||||
'stroke_width': self.stroke_width,
|
||||
'fill_color': self.fill_color,
|
||||
'show_label': self.show_label,
|
||||
'show_geometry': self.show_geometry,
|
||||
'interactive': self.interactive,
|
||||
'point_icon': self.point_icon,
|
||||
'external_url': self.external_url,
|
||||
'extra_data': self.extra_data,
|
||||
})
|
||||
result['level_id'] = self.level_id
|
||||
return result
|
|
@ -4,7 +4,7 @@ from pydantic import Discriminator
|
|||
from pydantic import Field as APIField
|
||||
from pydantic import NonNegativeFloat, PositiveFloat, PositiveInt
|
||||
|
||||
from c3nav.api.schema import BaseSchema, GeometrySchema, PointSchema
|
||||
from c3nav.api.schema import BaseSchema, GeometrySchema, PointSchema, AnyGeometrySchema
|
||||
from c3nav.api.utils import NonEmptyStr
|
||||
from c3nav.mapdata.models import LocationGroup
|
||||
from c3nav.mapdata.schemas.model_base import (AnyLocationID, AnyPositionID, CustomLocationID, DjangoModelSchema,
|
||||
|
@ -318,6 +318,27 @@ class DynamicLocationSchema(SpecificLocationSchema, DjangoModelSchema):
|
|||
pass
|
||||
|
||||
|
||||
class DataOverlaySchema(TitledSchema, DjangoModelSchema):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
class DataOverlayFeatureSchema(TitledSchema, DjangoModelSchema):
|
||||
geometry: AnyGeometrySchema
|
||||
level_id: PositiveInt
|
||||
stroke_color: Optional[str]
|
||||
stroke_width: Optional[float]
|
||||
fill_color: Optional[str]
|
||||
show_label: bool
|
||||
show_geometry: bool
|
||||
interactive: bool
|
||||
point_icon: Optional[str]
|
||||
external_url: Optional[str]
|
||||
extra_data: Optional[dict[str, str]]
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
class SourceSchema(WithAccessRestrictionSchema, DjangoModelSchema):
|
||||
"""
|
||||
A source image that can be traced in the editor.
|
||||
|
@ -653,7 +674,6 @@ SlimLocationSchema = Annotated[
|
|||
Discriminator("locationtype"),
|
||||
]
|
||||
|
||||
|
||||
listable_location_definitions = schema_definitions(
|
||||
(LevelSchema, SpaceSchema, AreaSchema, POISchema, DynamicLocationSchema, LocationGroupSchema)
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.utils.functional import lazy
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext_lazy
|
||||
|
||||
from c3nav.mapdata.models import DataOverlay
|
||||
from c3nav.mapdata.models.access import AccessPermission, AccessRestriction
|
||||
from c3nav.mapdata.models.locations import Position
|
||||
|
||||
|
@ -30,6 +31,18 @@ def get_user_data(request):
|
|||
})
|
||||
if request.user.is_authenticated:
|
||||
result['title'] = request.user.username
|
||||
|
||||
# TODO: permissions for overlays
|
||||
result.update({
|
||||
'overlays': [{
|
||||
'id': overlay.pk,
|
||||
'name': overlay.title,
|
||||
'group': None, # TODO
|
||||
'stroke_color': overlay.stroke_color,
|
||||
'stroke_width': overlay.stroke_width,
|
||||
'fill_color': overlay.fill_color,
|
||||
} for overlay in DataOverlay.objects.all()]
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
@ -1058,7 +1058,7 @@ main:not([data-view=route-result]) #route-summary {
|
|||
font-size: 20px;
|
||||
}
|
||||
|
||||
.leaflet-bar, .leaflet-touch .leaflet-bar, .leaflet-control-key {
|
||||
.leaflet-bar, .leaflet-touch .leaflet-bar, .leaflet-control-key, .leaflet-control-overlays {
|
||||
overflow: hidden;
|
||||
background-color: var(--color-control-background);
|
||||
border-radius: var(--border-radius-leaflet-control);
|
||||
|
@ -1676,10 +1676,10 @@ blink {
|
|||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.leaflet-control-key {
|
||||
.leaflet-control-key, .leaflet-control-overlays {
|
||||
background-clip: padding-box;
|
||||
|
||||
&.leaflet-control-key-expanded > .collapsed-toggle {
|
||||
&.leaflet-control-key-expanded > .collapsed-toggle, &.leaflet-control-overlays-expanded > .collapsed-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -1699,7 +1699,6 @@ blink {
|
|||
|
||||
&::before {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
content: 'legend_toggle';
|
||||
font-size: 26px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
@ -1736,23 +1735,36 @@ blink {
|
|||
}
|
||||
}
|
||||
|
||||
&.leaflet-control-key-expanded > .pin-toggle {
|
||||
&.leaflet-control-key-expanded > .pin-toggle, &.leaflet-control-overlays-expanded > .pin-toggle {
|
||||
display: block;
|
||||
.leaflet-touch & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
> .content {
|
||||
display: none;
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 2rem 1fr;
|
||||
padding: 1rem 4rem 1rem 1rem;
|
||||
|
||||
.leaflet-touch & {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.leaflet-control-key-expanded > .content {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.leaflet-control-key {
|
||||
> .collapsed-toggle::before {
|
||||
content: 'legend_toggle';
|
||||
}
|
||||
|
||||
> .content {
|
||||
gap: 1rem;
|
||||
grid-template-columns: 2rem 1fr;
|
||||
|
||||
> .key {
|
||||
display: grid;
|
||||
|
@ -1774,6 +1786,59 @@ blink {
|
|||
}
|
||||
}
|
||||
|
||||
.leaflet-control-overlays {
|
||||
> .collapsed-toggle::before {
|
||||
content: 'stacks';
|
||||
}
|
||||
|
||||
> .content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.overlay-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
content: 'arrow_right';
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
margin-left: 3ch;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-group.expanded h4::before {
|
||||
content: 'arrow_drop_down';
|
||||
}
|
||||
|
||||
.overlay-group:not(.expanded) label {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.leaflet-control-overlays-expanded > .content {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-top.leaflet-right {
|
||||
z-index: 2000;
|
||||
}
|
||||
|
@ -1799,3 +1864,42 @@ blink {
|
|||
margin-top: var(--control-container-minus-size);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-point-icon {
|
||||
> span {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-family: 'Material Symbols Outlined';
|
||||
}
|
||||
}
|
||||
|
||||
.data-overlay-popup {
|
||||
.leaflet-popup-content {
|
||||
margin: 0;
|
||||
|
||||
> h4, a {
|
||||
margin: 8px 12px 4px;
|
||||
}
|
||||
|
||||
> table {
|
||||
width: calc(100% + 2px);
|
||||
margin: 4px -2px;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
&:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1449,6 +1449,7 @@ c3nav = {
|
|||
c3nav._routeLayers = {};
|
||||
c3nav._routeLayerBounds = {};
|
||||
c3nav._userLocationLayers = {};
|
||||
c3nav._overlayLayers = {};
|
||||
c3nav._firstRouteLevel = null;
|
||||
c3nav._labelLayer = L.LayerGroup.collision({margin: 5}).addTo(c3nav.map);
|
||||
for (i = c3nav.levels.length - 1; i >= 0; i--) {
|
||||
|
@ -1458,6 +1459,7 @@ c3nav = {
|
|||
c3nav._locationLayers[level[0]] = L.layerGroup().addTo(layerGroup);
|
||||
c3nav._routeLayers[level[0]] = L.layerGroup().addTo(layerGroup);
|
||||
c3nav._userLocationLayers[level[0]] = L.layerGroup().addTo(layerGroup);
|
||||
c3nav._overlayLayers[level[0]] = L.layerGroup().addTo(layerGroup);
|
||||
}
|
||||
c3nav._levelControl.finalize();
|
||||
c3nav._levelControl.setLevel(c3nav.initial_level);
|
||||
|
@ -1480,6 +1482,8 @@ c3nav = {
|
|||
position: 'bottomright'
|
||||
}).addTo(c3nav.map);
|
||||
|
||||
c3nav._update_overlays();
|
||||
|
||||
c3nav.map.on('click', c3nav._click_anywhere);
|
||||
|
||||
c3nav.schedule_fetch_updates();
|
||||
|
@ -1855,12 +1859,34 @@ c3nav = {
|
|||
_set_user_data: function (data) {
|
||||
c3nav_api.authenticate();
|
||||
c3nav.user_data = data;
|
||||
c3nav._update_overlays();
|
||||
var $user = $('header #user');
|
||||
$user.find('span').text(data.title);
|
||||
$user.find('small').text(data.subtitle || '');
|
||||
$('.position-buttons').toggle(data.has_positions);
|
||||
if (window.mobileclient) mobileclient.setUserData(JSON.stringify(data));
|
||||
},
|
||||
_current_overlays_key: null,
|
||||
_update_overlays: function () {
|
||||
if (!c3nav.map) return;
|
||||
|
||||
const key = c3nav.user_data.overlays.map(o => o.id).join(',');
|
||||
if (key === c3nav._current_overlays_key) return;
|
||||
c3nav._current_overlays_key = key;
|
||||
|
||||
const control = new OverlayControl({levels: c3nav._overlayLayers});
|
||||
for (const overlay of c3nav.user_data.overlays) {
|
||||
control.addOverlay(new DataOverlay(overlay));
|
||||
}
|
||||
|
||||
if (c3nav._overlayControl) {
|
||||
c3nav.map.removeControl(c3nav._overlayControl);
|
||||
}
|
||||
|
||||
if (c3nav.user_data.overlays.length > 0) {
|
||||
c3nav._overlayControl = control.addTo(c3nav.map);
|
||||
}
|
||||
},
|
||||
|
||||
_hasLocationPermission: undefined,
|
||||
hasLocationPermission: function (nocache) {
|
||||
|
@ -2489,7 +2515,7 @@ KeyControl = L.Control.extend({
|
|||
this._pin = L.DomUtil.create('div', 'pin-toggle material-symbols', this._container);
|
||||
this._pin.classList.toggle('active', pinned);
|
||||
this._pin.innerText = 'push_pin';
|
||||
this._collapsed = L.DomUtil.create('a', 'collapsed-toggle leaflet-control-key-toggle', this._container);
|
||||
this._collapsed = L.DomUtil.create('a', 'collapsed-toggle', this._container);
|
||||
this._collapsed.href = '#';
|
||||
this._expanded = pinned;
|
||||
this._pinned = pinned;
|
||||
|
@ -2502,7 +2528,6 @@ KeyControl = L.Control.extend({
|
|||
}
|
||||
|
||||
|
||||
|
||||
if (L.Browser.touch) {
|
||||
this._pinned = false;
|
||||
console.log('installing touch handlers')
|
||||
|
@ -2588,6 +2613,162 @@ KeyControl = L.Control.extend({
|
|||
},
|
||||
});
|
||||
|
||||
OverlayControl = L.Control.extend({
|
||||
options: {position: 'topright', addClasses: '', levels: {}},
|
||||
_overlays: {},
|
||||
_groups: {},
|
||||
_initialActiveOverlays: null,
|
||||
_initialCollapsedGroups: null,
|
||||
|
||||
initialize: function ({levels, ...config}) {
|
||||
this.config = config;
|
||||
this._levels = levels;
|
||||
},
|
||||
|
||||
onAdd: function () {
|
||||
this._initialActiveOverlays = JSON.parse(localStorage.getItem('c3nav.overlays.active-overlays') ?? '[]');
|
||||
this._initialCollapsedGroups = JSON.parse(localStorage.getItem('c3nav.overlays.collapsed-groups') ?? '[]');
|
||||
const pinned = JSON.parse(localStorage.getItem('c3nav.overlays.pinned') ?? 'false');
|
||||
|
||||
this._container = L.DomUtil.create('div', 'leaflet-control-overlays ' + this.options.addClasses);
|
||||
this._container.classList.toggle('leaflet-control-overlays-expanded', pinned);
|
||||
this._content = L.DomUtil.create('div', 'content');
|
||||
const collapsed = L.DomUtil.create('div', 'collapsed-toggle');
|
||||
this._pin = L.DomUtil.create('div', 'pin-toggle material-symbols');
|
||||
this._pin.classList.toggle('active', pinned);
|
||||
this._pin.innerText = 'push_pin';
|
||||
this._container.append(this._pin, this._content, collapsed);
|
||||
this._expanded = pinned;
|
||||
this._pinned = pinned;
|
||||
|
||||
if (!L.Browser.android) {
|
||||
L.DomEvent.on(this._container, {
|
||||
mouseenter: this.expand,
|
||||
mouseleave: this.collapse
|
||||
}, this);
|
||||
}
|
||||
|
||||
if (!L.Browser.touch) {
|
||||
L.DomEvent.on(this._container, 'focus', this.expand, this);
|
||||
L.DomEvent.on(this._container, 'blur', this.collapse, this);
|
||||
}
|
||||
|
||||
for (const overlay of this._initialActiveOverlays) {
|
||||
if (overlay in this._overlays) {
|
||||
this._overlays[overlay].visible = true;
|
||||
this._overlays[overlay].enable(this._levels);
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of this._initialCollapsedGroups) {
|
||||
if (group in this._groups) {
|
||||
this._groups[group].expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.render();
|
||||
|
||||
$(this._container).on('change', 'input[type=checkbox]', e => {
|
||||
this._overlays[e.target.dataset.id].visible = e.target.checked;
|
||||
this.updateOverlay(e.target.dataset.id);
|
||||
});
|
||||
$(this._container).on('click', 'div.pin-toggle', e => {
|
||||
this.togglePinned();
|
||||
});
|
||||
$(this._container).on('click', '.content h4', e => {
|
||||
this.toggleGroup(e.target.parentElement.dataset.group);
|
||||
});
|
||||
$(this._container).on('mousedown pointerdown wheel', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
return this._container;
|
||||
},
|
||||
|
||||
addOverlay: function (overlay) {
|
||||
this._overlays[overlay.id] = overlay;
|
||||
if (overlay.group in this._groups) {
|
||||
this._groups[overlay.group].overlays.push(overlay);
|
||||
} else {
|
||||
this._groups[overlay.group] = {
|
||||
expanded: this._initialCollapsedGroups === null || !this._initialCollapsedGroups.includes(overlay.group),
|
||||
overlays: [overlay],
|
||||
};
|
||||
}
|
||||
this.render();
|
||||
},
|
||||
|
||||
updateOverlay: function (id) {
|
||||
const overlay = this._overlays[id];
|
||||
if (overlay.visible) {
|
||||
overlay.enable(this._levels);
|
||||
} else {
|
||||
overlay.disable(this._levels);
|
||||
}
|
||||
const activeOverlays = Object.keys(this._overlays).filter(k => this._overlays[k].visible);
|
||||
localStorage.setItem('c3nav.overlays.active-overlays', JSON.stringify(activeOverlays));
|
||||
},
|
||||
|
||||
render: function () {
|
||||
if (!this._content) return;
|
||||
const groups = document.createDocumentFragment();
|
||||
for (const group in this._groups) {
|
||||
const group_container = document.createElement('div');
|
||||
group_container.classList.add('overlay-group');
|
||||
if (this._groups[group].expanded) {
|
||||
group_container.classList.add('expanded');
|
||||
}
|
||||
this._groups[group].el = group_container;
|
||||
group_container.dataset.group = group;
|
||||
const title = document.createElement('h4');
|
||||
title.innerText = group;
|
||||
group_container.append(title);
|
||||
for (const overlay of this._groups[group].overlays) {
|
||||
const label = document.createElement('label');
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.dataset.id = overlay.id;
|
||||
if (overlay.visible) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
label.append(checkbox, overlay.name);
|
||||
group_container.append(label);
|
||||
}
|
||||
groups.append(group_container);
|
||||
}
|
||||
this._content.replaceChildren(...groups.children);
|
||||
},
|
||||
|
||||
expand: function () {
|
||||
if (this._pinned) return;
|
||||
this._expanded = true;
|
||||
this._container.classList.add('leaflet-control-overlays-expanded');
|
||||
return this;
|
||||
},
|
||||
|
||||
collapse: function () {
|
||||
if (this._pinned) return;
|
||||
this._expanded = false;
|
||||
this._container.classList.remove('leaflet-control-overlays-expanded');
|
||||
return this;
|
||||
},
|
||||
|
||||
toggleGroup: function (name) {
|
||||
const group = this._groups[name];
|
||||
group.expanded = !group.expanded;
|
||||
group.el.classList.toggle('expanded', group.expanded);
|
||||
const collapsedGroups = Object.keys(this._groups).filter(k => !this._groups[k].expanded);
|
||||
localStorage.setItem('c3nav.overlays.collapsed-groups', JSON.stringify(collapsedGroups));
|
||||
},
|
||||
|
||||
togglePinned: function () {
|
||||
this._pinned = !this._pinned;
|
||||
if (this._pinned) {
|
||||
this._expanded = true;
|
||||
}
|
||||
this._pin.classList.toggle('active', this._pinned);
|
||||
localStorage.setItem('c3nav.overlays.pinned', JSON.stringify(this._pinned));
|
||||
},
|
||||
});
|
||||
|
||||
var SvgIcon = L.Icon.extend({
|
||||
options: {
|
||||
|
@ -2646,3 +2827,98 @@ var SvgIcon = L.Icon.extend({
|
|||
return svgEl;
|
||||
},
|
||||
});
|
||||
|
||||
class DataOverlay {
|
||||
levels = null;
|
||||
|
||||
constructor(options) {
|
||||
this.id = options.id;
|
||||
this.name = options.name;
|
||||
this.group = options.group ?? 'ungrouped';
|
||||
this.default_stroke_color = options.stroke_color;
|
||||
this.default_stroke_width = options.stroke_width;
|
||||
this.default_fill_color = options.fill_color;
|
||||
}
|
||||
|
||||
async create() {
|
||||
const features = await c3nav_api.get(`mapdata/overlays/${this.id}/`);
|
||||
|
||||
const levels = {};
|
||||
for (const feature of features) {
|
||||
const level_id = feature.level_id;
|
||||
if (!(level_id in levels)) {
|
||||
levels[level_id] = L.layerGroup([]);
|
||||
}
|
||||
const style = {
|
||||
'color': feature.stroke_color ?? this.default_stroke_color ?? 'var(--color-map-overlay)',
|
||||
'weight': feature.stroke_width ?? this.default_stroke_width ?? 1,
|
||||
'fillColor': feature.fill_color ?? this.default_fill_color ?? 'var(--color-map-overlay)',
|
||||
};
|
||||
const layer = L.geoJson(feature.geometry, {
|
||||
style,
|
||||
interactive: feature.interactive,
|
||||
pointToLayer: (geom, latlng) => {
|
||||
if (feature.point_icon !== null) {
|
||||
return L.marker(latlng, {
|
||||
title: feature.title,
|
||||
icon: L.divIcon({
|
||||
className: 'overlay-point-icon',
|
||||
html: `<span style="color: ${style.color}">${feature.point_icon}</span>`,
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12],
|
||||
})
|
||||
});
|
||||
} else {
|
||||
return L.circleMarker(latlng, {
|
||||
title: feature.title,
|
||||
...style
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (feature.interactive) {
|
||||
layer.bindPopup(() => {
|
||||
let html = `<h4>${feature.title}</h4>`;
|
||||
if (feature.external_url != null) {
|
||||
html += `<a href="${feature.external_url}" target="_blank">open external link</a>`;
|
||||
}
|
||||
if (feature.extra_data != null) {
|
||||
html += '<table>';
|
||||
for (const key in feature.extra_data) {
|
||||
html += `<tr><th>${key}</th><td>${feature.extra_data[key]}</td></tr>`;
|
||||
}
|
||||
|
||||
html += '</table>';
|
||||
}
|
||||
return html;
|
||||
}, {
|
||||
className: 'data-overlay-popup'
|
||||
});
|
||||
}
|
||||
levels[level_id].addLayer(layer);
|
||||
}
|
||||
|
||||
this.levels = levels;
|
||||
}
|
||||
|
||||
async enable(levels) {
|
||||
if (!this.levels) {
|
||||
await this.create();
|
||||
}
|
||||
for (const id in levels) {
|
||||
if (id in this.levels) {
|
||||
levels[id].addLayer(this.levels[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disable(levels) {
|
||||
for (const id in levels) {
|
||||
if (id in this.levels) {
|
||||
levels[id].removeLayer(this.levels[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue