Skip to content

Commit

Permalink
Feature: Edit selection (#1776)
Browse files Browse the repository at this point in the history
* Work in progress edit selection

* autolint

* Editor takes selection from featureinfo

* Select from featureinfo when editor is active

* Remved some stupid lint in comment

* Lint formatting

* Deleted unnecessary .bak-file

---------

Co-authored-by: Stefan Forsgren <stefan@forsgren@xlent.se>
  • Loading branch information
steff-o and Stefan Forsgren authored Aug 21, 2023
1 parent 1a5202e commit 7d2eda2
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 45 deletions.
44 changes: 43 additions & 1 deletion src/controls/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const Editor = function Editor(options = {}) {
let viewer;
let isVisible = isActive;
let toolbarVisible = false;
/** Keeps track of the last selected item in featureinfo. We need to use our own variable for this
* in order to determine if editor got activated directly from featureinfo or some other tool has been active between. */
let lastSelectedItem;

/** The handler were all state is kept */
let editHandler;
Expand All @@ -26,6 +29,16 @@ const Editor = function Editor(options = {}) {
// There are some serious event dependencies between viewer, editor, edithandler, editortoolbar, editorlayers, dropdown and editorbutton,
// which makes it almost impossible to do things in correct order.
isVisible = detail.active;
// Actually, if we're going visible
if (isVisible) {
if (lastSelectedItem && lastSelectedItem.getLayer().get('editable') && !lastSelectedItem.getLayer().get('isTable')) {
// Set a preselected feature. No use to set layer in handler as toolbar keeps state that will override a change of layer in handler anyway
// when editor toolbar becomes visible.
editHandler.preselectFeature(lastSelectedItem.getFeature());
// Have to set layer in toolbar instead of handler.
editorToolbar.changeActiveLayer(lastSelectedItem.getLayer().get('name'));
}
}
viewer.dispatch('toggleClickInteraction', detail);
};

Expand Down Expand Up @@ -76,10 +89,39 @@ const Editor = function Editor(options = {}) {
});
editHandler = EditHandler(handlerOptions, viewer);

// Set up eventhandler for when featureinfo selects (or highlights) a feature
const featureInfo = viewer.getFeatureinfo();
featureInfo.on('changeselection', detail => {
if (isVisible) {
// This can only happen if featureInfo.ShowFeatureInfo, featureInfo.showInfow or featureInfo.render is called
// from api, as featureInfo component can not be active when editor is so the user can not click in the map to select anything.
if (detail && detail.getLayer().get('editable') && !detail.getLayer().get('isTable')) {
// Set a preselected feature.
editHandler.preselectFeature(detail.getFeature());
// Actually change active layer.
editHandler.setActiveLayer(detail.getLayer().get('name'));
// Have to update state in toolbar as well.
editorToolbar.changeActiveLayer(detail.getLayer().get('name'));
// Clear featureinfo to close the popup which otherwise would just be annoying
featureInfo.clear();
}
} else {
lastSelectedItem = detail;
}
});

// Set up eventhandler for when featureinfo clears selection
// Event is sent from featureinfo when popup etc is closed or cleared, but not when tool changes
featureInfo.on('clearselection', () => {
lastSelectedItem = null;
});

viewer.on('toggleClickInteraction', (detail) => {
if (detail.name === 'editor' && detail.active) {
editorButton.dispatch('change', { state: 'active' });
} else {
// Someone else got active. Ditch the last selected item as we don't go directly from featureinfo to edit
lastSelectedItem = null;
editorButton.dispatch('change', { state: 'initial' });
}
});
Expand Down Expand Up @@ -131,7 +173,7 @@ const Editor = function Editor(options = {}) {
editFeatureAttributes,
deleteFeature,
changeActiveLayer: (layerName) => {
// Only need to actually cahne layer if editor is active. Otherwise state is just set in toolbar and will
// Only need to actually change layer if editor is active. Otherwise state is just set in toolbar and will
// activate set layer when toggled visible
if (isVisible) {
editHandler.setActiveLayer(layerName);
Expand Down
37 changes: 29 additions & 8 deletions src/controls/editor/edithandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ let allowEditGeometry;
let breadcrumbs = [];
let autoCreatedFeature = false;
let infowindowCmp = false;
let preselectedFeature;

function isActive() {
// FIXME: this only happens at startup as they are set to null on closing. If checking for null/falsley/not truely it could work as isVisible with
Expand Down Expand Up @@ -571,6 +572,12 @@ function setInteractions(drawType) {
}
});
}

if (preselectedFeature) {
select.getFeatures().push(preselectedFeature);
}
// Clear it so we won't get stuck on this feature. This makes it unnecessary to clear it anywhere else.
preselectedFeature = null;
if (allowEditGeometry) {
modify = new Modify({
features: select.getFeatures()
Expand All @@ -592,27 +599,33 @@ function setInteractions(drawType) {
// If snap should be active then add snap internactions for all snap layers
hasSnap = editLayer.get('snap');
if (hasSnap) {
// FIXME: selection will almost certainly be empty as featureInfo is cleared
const selectionSource = featureInfo.getSelectionLayer().getSource();
const snapSources = editLayer.get('snapLayers') ? getSnapSources(editLayer.get('snapLayers')) : [editLayer.get('source')];
snapSources.push(selectionSource);
snap = addSnapInteraction(snapSources);
}
}

/** Closes all modals and resets breadcrumbs */
function closeAllModals() {
// Close all modals first to get rid of tags in DOM
// Close all modals before resetting breadcrumbs to get rid of tags in DOM
if (modal) modal.closeModal();
modal = null;
breadcrumbs.forEach(br => {
if (br.modal) br.modal.closeModal();
});
if (breadcrumbs.length > 0) {
currentLayer = breadcrumbs[0].layerName;
title = breadcrumbs[0].title;
attributes = breadcrumbs[0].attributes;
}
breadcrumbs = [];
}

function setEditLayer(layerName) {
// It is not possible to actually change layer while having breadcrubs as all modals must be closed, which will
// pop off all breadcrumbs.
// But just in case something changes, reset the breadcrumbs when a new layer is edited.
// Close all modals first and restore state. This can only happen if calling using api, as
// the modal prevents user from clicking in the map conrol
closeAllModals();
currentLayer = layerName;
setAllowedOperations();
Expand Down Expand Up @@ -1388,10 +1401,8 @@ function editAttributesDialogApi(featureId, layerName = null) {
const layer = viewer.getLayer(layerName);
const feature = layer.getSource().getFeatureById(featureId);
// Hijack the current layer for a while. If there's a modal visible it is closed (without saving) as editAttributes can not handle
// multiple dialogs for the same layer so to be safe we always close. Technically the user can not
// call this function when a modal is visible, as they can't click anywhere.
// multiple dialogs for the same layer so to be safe we always close.
// Restoring currentLayer is performed in onModalClosed(), as we can't await the modal.
// Close all modals and eat all breadcrumbs
closeAllModals();
// If editing in another layer, add a breadcrumb to restore layer when modal is closed.
if (layerName && layerName !== currentLayer) {
Expand Down Expand Up @@ -1463,6 +1474,15 @@ function onDeleteChild(e) {
deleteFeature(e.detail.feature, e.detail.layer).then(() => refreshRelatedTablesForm(e.detail.parentFeature));
}

/**
* Sets a feature that will be active for editing when editor is activated. When the edit session starts, the feature's layer must
* be active and that state is kept in the editor toolbar and sent through an event, so you better update toolbar as well.
* @param {any} feature
*/
function preselectFeature(feature) {
preselectedFeature = feature;
}

/**
* Creates the handler. It is used as sort of a singelton, but in theory there could be many handlers.
* It communicates with the editor toolbar and forms using DOM events, which makes it messy to have more than one instance as they would use the same events.
Expand Down Expand Up @@ -1508,6 +1528,7 @@ export default function editHandler(options, v) {
createFeature: createFeatureApi,
editAttributesDialog: editAttributesDialogApi,
deleteFeature: deleteFeatureApi,
setActiveLayer: setActiveLayerApi
setActiveLayer: setActiveLayerApi,
preselectFeature
};
}
68 changes: 51 additions & 17 deletions src/featureinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import getAttributes, { getContent, featureinfotemplates } from './getattributes
import relatedtables from './utils/relatedtables';

const styleTypes = StyleTypes();
let selectionLayer;

const Featureinfo = function Featureinfo(options = {}) {
const {
Expand All @@ -32,6 +31,7 @@ const Featureinfo = function Featureinfo(options = {}) {
autoplay = false
} = options;

let selectionLayer;
let identifyTarget;
let overlay;
let items;
Expand All @@ -47,14 +47,55 @@ const Featureinfo = function Featureinfo(options = {}) {
const savedFeature = savedPin || savedSelection || undefined;
const uiOutput = 'infowindow' in options ? options.infowindow : 'overlay';

/** Dispatches a clearselectionevent. Should be emitted when window is closed or cleared but not when featureinfo is closed as a result of tool change. */
const dispatchClearSelection = function dispatchClearSelection() {
component.dispatch('clearselection', null);
};

/** Eventhandler for Selectionmanager's clear event. */
function onSelectionManagerClear() {
// Not much do to, just dispatch event as our own.
dispatchClearSelection();
}

/**
* Clears selection in all possible infowindows (overlay, sidebar and infoWindow) and closes the windows
* @param {any} supressEvent Set to true when closing as a result of tool change to supress sending clearselection event
*/
const clear = function clear(supressEvent) {
selectionLayer.clear();
// Sidebar is static and always present.
sidebar.setVisibility(false);
// check needed for when sidebar or overlay are selected.
if (overlay) {
viewer.removeOverlays(overlay);
}
if (selectionManager) {
// clearSelection will fire an cleared event, but we don't want our handler to emit a clear event as we are the one closing,
// so we stop listening for a while.
selectionManager.un('cleared', onSelectionManagerClear);
// This actually closes infowindow as infowindow closes automatically when selection is empty.
selectionManager.clearSelection();
selectionManager.on('cleared', onSelectionManagerClear);
}
if (!supressEvent) {
dispatchClearSelection();
}
};

/** Callback called from overlay and sidebar when they are closed by their close buttons */
function onInfoClosed() {
clear(false);
}

function setUIoutput(v) {
switch (uiOutput) {
case 'infowindow':
identifyTarget = 'infowindow';
break;

case 'sidebar':
sidebar.init(v);
sidebar.init(v, { closeCb: onInfoClosed });
identifyTarget = 'sidebar';
break;

Expand All @@ -64,16 +105,6 @@ const Featureinfo = function Featureinfo(options = {}) {
}
}

const clear = function clear() {
selectionLayer.clear();
// check needed for when sidebar or overlay are selected.
if (selectionManager) selectionManager.clearSelection();
sidebar.setVisibility(false);
if (overlay) {
viewer.removeOverlays(overlay);
}
};

// FIXME: overly complex. Don't think layer can be a string anymore
const getTitle = function getTitle(item) {
let featureinfoTitle;
Expand Down Expand Up @@ -379,14 +410,14 @@ const Featureinfo = function Featureinfo(options = {}) {
const map = viewer.getMap();

items = identifyItems;
clear();
clear(false);
// FIXME: variable is overwritten in next row
let content = items.map((i) => i.content).join('');
content = '<div id="o-identify"><div id="o-identify-carousel" class="flex"></div></div>';
switch (target) {
case 'overlay':
{
popup = Popup(`#${viewer.getId()}`);
popup = Popup(`#${viewer.getId()}`, { closeCb: onInfoClosed });
popup.setContent({
content,
title: getTitle(items[0])
Expand Down Expand Up @@ -607,10 +638,10 @@ const Featureinfo = function Featureinfo(options = {}) {
const serverResult = data || [];
const result = serverResult.concat(clientResult);
if (result.length > 0) {
selectionLayer.clear();
selectionLayer.clear(false);
render(result, identifyTarget, evt.coordinate);
} else if (selectionLayer.getFeatures().length > 0 || (identifyTarget === 'infowindow' && selectionManager.getNumberOfSelectedItems() > 0)) {
clear();
clear(false);
} else if (pinning) {
const resolution = map.getView().getResolution();
sidebar.setVisibility(false);
Expand All @@ -631,7 +662,7 @@ const Featureinfo = function Featureinfo(options = {}) {
if (state) {
map.on(clickEvent, onClick);
} else {
clear();
clear(true);
map.un(clickEvent, onClick);
}
};
Expand All @@ -655,9 +686,12 @@ const Featureinfo = function Featureinfo(options = {}) {
// Re dispatch selectionmanager's event as our own
if (selectionManager) {
selectionManager.on('highlight', evt => dispatchToggleFeatureEvent(evt));
selectionManager.on('cleared', onSelectionManagerClear);
}
map.on(clickEvent, onClick);
viewer.on('toggleClickInteraction', (detail) => {
// This line of beauty makes feature info active if explicitly set active of another control yields active state.
// which effectively makes this the default tool.
if ((detail.name === 'featureinfo' && detail.active) || (detail.name !== 'featureinfo' && !detail.active)) {
setActive(true);
} else {
Expand Down
42 changes: 30 additions & 12 deletions src/popup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { dom } from './ui';
import Featureinfo from './featureinfo';

function render(target) {
const pop = `<div id="o-popup">
Expand Down Expand Up @@ -58,27 +57,46 @@ function setContent(config) {
}
}

function closePopup() {
/**
* Closes the window and optionally calls a callback set at init
* @param {any} cb
*/
function closePopupInternal(cb) {
setVisibility(false);
Featureinfo().clear();
if (cb) {
cb();
}
}

function bindUIActions() {
const closeel = document.querySelector('#o-popup .o-popup #o-close-button');
closeel.addEventListener('click', (evt) => {
closePopup();
evt.preventDefault();
});
}
/**
* Creates a new popup and adds it to the dom.
* @param {any} target id of parent DOM object
* @param {Object} opts options.
* @param {function} opts.closeCb Function without parameters to be called when popup is closed from close button.
*/
export default function popup(target, opts = {}) {
const {
closeCb
} = opts;

function bindUIActions() {
const closeel = document.querySelector('#o-popup .o-popup #o-close-button');
closeel.addEventListener('click', (evt) => {
closePopupInternal(closeCb);
evt.preventDefault();
});
}

export default function popup(target) {
render(target);
bindUIActions();

return {
getEl,
setVisibility,
setTitle,
setContent,
closePopup
closePopup: () => {
closePopupInternal(closeCb);
}
};
}
1 change: 1 addition & 0 deletions src/selectionmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ const Selectionmanager = function Selectionmanager(options = {}) {

function clearSelection() {
selectedItems.clear();
component.dispatch('cleared', null);
}

function featureStyler(feature) {
Expand Down
Loading

0 comments on commit 7d2eda2

Please sign in to comment.