Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: spinner and alert on no hits #39

Merged
merged 1 commit into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ currentLayerConfig | Index of the selected layerConfiguration at startup | 0
pointBufferFactor | How much a point should be buffered before intersecting when using click tool. Does not apply if active configuration is All visible, as that uses featureInfo hitTolerance setting. | 1
bufferSymbol | Name of a symbol in origo configuration to use as symbol for buffered objects. Symbol is always a polygon. | A built-in symbol
chooseSymbol | Name of a symbol in origo configuration to use as symbol for highlighted features when choosing which feature to buffer. Symbol should handle point, line and polygon. | A built-in symbol
warnOnNoHits | Wether an alert should be displayed or not when no features to select is found. (bool) | false

#### layerConfiguration
A layerConfiguration specifies in which layers features are selected. The default behaviour is to select features in all currently visible
Expand Down
181 changes: 128 additions & 53 deletions src/multiselect.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ const Multiselect = function Multiselect(options = {}) {
const showClearButton = options.showClearButton === true;
const showAddToSelectionButton = options.showAddToSelectionButton === true;
let addToSelection = options.addToSelection !== false;
const warnOnNoHits = options.warnOnNoHits === true;
/**
* True if origo exposes spinner. Older origo versions don't expose it. In that case we don't do anything.
*/
const hasSpinner = !!Origo.Loader;
/**
* Handle that spinner uses to cancel a pending spinner. There can be only one spinner at any given time as it is modal.
*/
let timerId = 0;

function setActive(state) {
isActive = state;
Expand Down Expand Up @@ -497,75 +506,132 @@ const Multiselect = function Multiselect(options = {}) {
return intersectingItems;
}

/**
* Displays a spinner. Call hideSpinner() when you are done, preferrably within a finally clause to avoid leaving spinner lingering around.
* Don't call several times without hide between, but the modal nature will prevent user from doing that.
*/
function showSpinner() {
// Show spinner if the Origo version exposes it.
if (hasSpinner) {
// Wait a little while before showing spinner to avoid flicking if request is quick.
// If there are no network requests (only strategy all WFS) chance that it will be finished within this time limit is very large.
// Could of course flick if requests take just a wee bit more than the delay, but in this way we
// get rid of the really quick ones.
// Drawback is if the timeconsuming operation is sync, the spinner won't be shown as the event won't be scheduled to run until
// cpu bound operation is finished.
timerId = setTimeout(() => {
Origo.Loader.show();
}, 100);
}
}

/**
* Hides the spinner. Safe to call even if no spinner is visible.
*/
function hideSpinner() {
if (hasSpinner) {
// Abort if it hasn't fired yet. Safe to do even if timer has fired so no need to check.
clearTimeout(timerId);
// No harm in hiding even if it hasn't been shown yet.
Origo.Loader.hide();
}
}

function showEmptyResultModal() {
if (warnOnNoHits) {
const mTarget = viewer.getId();
Origo.ui.Modal({
// Make title and content configurable for language support?
title: 'Observera!',
content: 'Sökningen gav inga träffar',
target: mTarget
});
}
}

/**
* Gets all features from the eligable layers intersecting the geometry and adds (or remove) them to SelectionManager.
* @param {any} geometry The geometry to intersect
* @param {any} remove true if selection should be removed insread of added
*/
async function updateSelectionManager(geometry, remove) {
showSpinner();
if (!addToSelection) {
clearSelection();
}
const promises = [];
let layers;
const extent = geometry.getExtent();

/**
* Recursively traverse all layers to discover all individual layers in group layers
* @param {any} layers
* @param {any} groupLayer
*/
function traverseLayers(tLayers, groupLayer) {
for (let i = 0; i < tLayers.length; i += 1) {
const currLayer = tLayers[i];
if (!shouldSkipLayer(currLayer)) {
if (currLayer.get('type') === 'GROUP') {
const subLayers = currLayer.getLayers().getArray();
traverseLayers(subLayers, currLayer);
} else if (geometry.getType() === 'GeometryCollection') {
// Explode geometry collections as they very well have disjoint extents, which would result in tons of false positives.
// TODO: Explode multiparts as well?
// It will result in more calls, but reduces scattered geometries in a large extent.
geometry.getGeometries().forEach(currGeo => promises.push(extractResultsForALayer(currLayer, groupLayer, currGeo.getExtent())));
} else {
promises.push(extractResultsForALayer(currLayer, groupLayer, extent));
try {
const promises = [];
let layers;
const extent = geometry.getExtent();

/**
* Recursively traverse all layers to discover all individual layers in group layers
* @param {any} layers
* @param {any} groupLayer
*/
function traverseLayers(tLayers, groupLayer) {
for (let i = 0; i < tLayers.length; i += 1) {
const currLayer = tLayers[i];
if (!shouldSkipLayer(currLayer)) {
if (currLayer.get('type') === 'GROUP') {
const subLayers = currLayer.getLayers().getArray();
traverseLayers(subLayers, currLayer);
} else if (geometry.getType() === 'GeometryCollection') {
// Explode geometry collections as they very well have disjoint extents, which would result in tons of false positives.
// TODO: Explode multiparts as well?
// It will result in more calls, but reduces scattered geometries in a large extent.
geometry.getGeometries().forEach(currGeo => promises.push(extractResultsForALayer(currLayer, groupLayer, currGeo.getExtent())));
} else {
promises.push(extractResultsForALayer(currLayer, groupLayer, extent));
}
}
}
}
}

if (currentLayerConfig.layers) {
// Use configured layers
layers = currentLayerConfig.layers.map(l => viewer.getLayer(l));
} else {
// Use queryable layers when no config exists (default behaviour)
layers = viewer.getQueryableLayers(true);
}
if (currentLayerConfig.layers) {
// Use configured layers
layers = currentLayerConfig.layers.map(l => viewer.getLayer(l));
} else {
// Use queryable layers when no config exists (default behaviour)
layers = viewer.getQueryableLayers(true);
}

// This call populates the promises array, so on the next line we can await it
traverseLayers(layers);
const items = await Promise.all(promises);
// Is an array of arrays, we want an array.
const allItems = items.flat();

// Narrow down selection to only contain thos whose actual geometry intersects the selection geometry.
// We could implement different spatial relations, i.e contains, is contained etc. But for now only intersect is supported.
const intersectingItems = getItemsIntersectingGeometry(allItems, geometry);

// Add them to selection
// handle removal for point when ctrl-click
if (remove) {
if (intersectingItems.length > 0) {
selectionManager.removeItems(intersectingItems);
// This call populates the promises array, so on the next line we can await it
traverseLayers(layers);

// Collect all respones
const items = await Promise.all(promises);

// Is an array of arrays, we want an array.
const allItems = items.flat();

// Narrow down selection to only contain thos whose actual geometry intersects the selection geometry.
// We could implement different spatial relations, i.e contains, is contained etc. But for now only intersect is supported.
const intersectingItems = getItemsIntersectingGeometry(allItems, geometry);

// Add them to selection
// handle removal for point when ctrl-click
if (remove) {
if (intersectingItems.length > 0) {
selectionManager.removeItems(intersectingItems);
}
} else if (intersectingItems.length === 1) {
selectionManager.addOrHighlightItem(intersectingItems[0]);
} else if (intersectingItems.length > 1) {
selectionManager.addItems(intersectingItems);
}
} else if (intersectingItems.length === 1) {
selectionManager.addOrHighlightItem(intersectingItems[0]);
} else if (intersectingItems.length > 1) {
selectionManager.addItems(intersectingItems);
// Notify user if result was empty to avoid them waiting for ever
if (intersectingItems.length === 0) {
showEmptyResultModal();
}
}
finally {
hideSpinner();
}
// TODO: Notify user if result was empty to avoid them waiting for ever
}



/**
* Selects features by an already selected feature (in a global variable) with a buffer.
* @param {any} fRadius
Expand Down Expand Up @@ -735,7 +801,8 @@ const Multiselect = function Multiselect(options = {}) {
}
// For backwards compability use featureInfo style when not using specific layer conf.
// The featureInfo style will honour the alternative featureInfo layer and radius configuration in the core
// also it unwinds clustering.
// also it unwinds clustering. But it does not support Multiselect alternative layers or extended WMS handling (which is not necessary
// as a WMS layer will work out of the box for points)
// Featureinfo in two steps. Concat serverside and clientside when serverside is finished
const pixel = evt.pixel;
const coordinate = evt.coordinate;
Expand All @@ -750,6 +817,7 @@ const Multiselect = function Multiselect(options = {}) {
}, viewer);
// Abort if clientResult is false
if (clientResult !== false) {
showSpinner();
Origo.getFeatureInfo.getFeaturesFromRemote({
coordinate,
layers,
Expand All @@ -759,6 +827,10 @@ const Multiselect = function Multiselect(options = {}) {
.then((data) => {
const serverResult = data || [];
const result = serverResult.concat(clientResult);
if (result.length === 0) {
// Notify user if result was empty to avoid them waiting for ever
showEmptyResultModal();
}
if (isCtrlKeyPressed) {
if (result.length > 0) {
selectionManager.removeItems(result);
Expand All @@ -772,7 +844,10 @@ const Multiselect = function Multiselect(options = {}) {
for (let i = 0; i < modalLinks.length; i += 1) {
viewer.getFeatureinfo().addLinkListener(modalLinks[i]);
}
});
})
.finally(() => {
hideSpinner();
}) ;
}
return false;
}
Expand Down