Skip to content

Commit

Permalink
Merge pull request #201 from Dataport/feature/gfi-multiple-features
Browse files Browse the repository at this point in the history
Feature/gfi multiple features
  • Loading branch information
dopenguin authored Nov 18, 2024
2 parents 8aa2fb3 + d4a0d10 commit 3e37965
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 136 deletions.
1 change: 1 addition & 0 deletions packages/clients/snowbox/src/mapConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export const mapConfiguration = {
gfi: {
mode: 'bboxDot',
activeLayerPath: 'plugin/layerChooser/activeMaskIds',
boxSelect: true,
layers: {
[uBahn]: {
geometry: true,
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/Gfi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

- Breaking: Upgrade `@masterportal/masterportalapi` from `2.8.0` to `2.40.0` and subsequently `ol` from `^7.1.0` to `^9.2.4`.
- Feature: Add new configuration parameter `isSelectable` that can be used to filter features to be unselectable.
- Feature: Add new configuration parameters `directSelect` and `boxSelect` to be able to select multiple features at once.
- Fix: Adjust documentation to properly describe optionality of configuration parameters.
- Fix: Add missing configuration parameters `featureList` and `maxFeatures` to the general documentation and `filterBy` and `format` to `gfi.gfiLayerConfiguration`
- Fix: Add missing entry of `gfiContentComponent` to `GfiGetters`.
- Refactor: Replace redundant prop-forwarding with `getters`.
- Refactor: Use core getter `clientWidth` instead of local computed value.
- Chore: expand on the description to `gfiContentComponent` in the Readme.md.
Expand Down
4 changes: 3 additions & 1 deletion packages/plugins/Gfi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ The GFI plugin can be used to fetch and optionally display GFI (GetFeatureInfo)

| fieldName | type | description |
| - | - | - |
| activeLayerPath | string? | Optional store path to array of active mask layer ids. If used with `LayerChooser`, setting this to `'plugin/layerChooser/activeMaskIds'` will result in an info text in the GFI box, should no layer be active. If used without `LayerChooser`, the active mask layers have to be provided by another plugin or the client. If not set, the GFI plugin may e.g. show an empty list, which may be confusing to some users. |
| coordinateSources | string[] | The GFI plugin will react to these coordinate positions in the store. This allows it to react to e.g. the address search of the pins plugin. Please see example configuration for the common use-cases. Please mind that, when referencing another plugin, that plugin must be in `addPlugins` before this one. |
| layers | Record<string, gfiLayerConfiguration> | Maps a string (must be a layer ID) to a behaviour configuration for that layer. |
| activeLayerPath | string? | Optional store path to array of active mask layer ids. If used with `LayerChooser`, setting this to `'plugin/layerChooser/activeMaskIds'` will result in an info text in the GFI box, should no layer be active. If used without `LayerChooser`, the active mask layers have to be provided by another plugin or the client. If not set, the GFI plugin may e.g. show an empty list, which may be confusing to some users. |
| afterLoadFunction | function (featuresByLayerId: Record<string, GeoJsonFeature[]>): Record<layerId, GeoJsonFeature[]>? | This method can be used to extend, filter, or otherwise modify a GFI result. |
| boxSelect | boolean? | If set to `true`, multiple features can be selected at once by using the modifier key (CTRL on Windows or Command on macOS) and dragging the mouse. Similar to `gfi.directSelect`, features can be added and removed by selection / unselecting them. The features need to be distinguishable by their properties for the functionality to properly work. Does not work together with `extendedMasterportalapiMarkers` of `@polar/core`. Defaults to `false`. |
| customHighlightStyle | customHighlighStyle? | If required a user can change the stroke and fill of the highlighted feature. The default style as seen in the example will be used for each part that is not customized. An empty object will return the complete default style while e.g. for an object without a configured fill the default fill will be applied. |
| directSelect | boolean? | If set to `true`, a feature can be selected without defining a value in `gfi.coordinateSources`. It is also possible to add multiple features to the selection by using the modifier key (CTRL on Windows or Command on macOS). To delesect a feature, simply reclick it with the modifier key pressed. To create a new selection, click anywhere else without pressing the modifier key. Be careful when using this parameter together with some values set in `coordinateSources` as it may lead to unexpected results. The features need to be distinguishable by their properties for the functionality to properly work. Does not work together with `extendedMasterportalapiMarkers` of `@polar/core`. Defaults to `false`. |
| featureList | featureList? | If defined, a list of available vector layer features is visible when no feature is selected. Only usable if `renderType` is set to `iconMenu` and `window` is set to `true` for at least one configured layer. |
| gfiContentComponent | VueConstructor? | Allows overriding the GfiContent.vue component for custom design and functionality. Coding knowledge is required to use this feature, as any implementation will have to rely upon the VueX store model. Please refer to the implementation. |
| maxFeatures | number? | Limits the viewable GFIs per layer by this number. The first n elements are chosen arbitrarily. Useful if you e.g. just want one result, or to limit an endless stream of returns to e.g. 10. Infinite by default. |
Expand Down
91 changes: 58 additions & 33 deletions packages/plugins/Gfi/src/store/actions/debouncedGfiRequest.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import debounce from 'lodash.debounce'
import { rawLayerList } from '@masterportal/masterportalapi'
import {
Feature as GeoJsonFeature,
GeoJsonProperties,
Geometry as GeoJsonGeometry,
} from 'geojson'
import { Feature as GeoJsonFeature } from 'geojson'
import {
GfiConfiguration,
MapConfig,
Expand All @@ -13,17 +9,25 @@ import {
import { Map, Feature } from 'ol'
import { Geometry } from 'ol/geom'
import VectorLayer from 'ol/layer/Vector'
import compare from 'just-compare'
import { addFeature } from '../../utils/displayFeatureLayer'
import { requestGfi } from '../../utils/requestGfi'
import sortFeatures from '../../utils/sortFeatures'
import { GfiGetters, GfiState } from '../../types'

interface GetFeatureInfoParameters {
coordinateOrExtent: [number, number] | [number, number, number, number]
modifierPressed?: boolean
}

type FeaturesByLayerId = Record<string, GeoJsonFeature[] | symbol>

const filterAndMapFeaturesToLayerIds = (
layerKeys: string[],
gfiConfiguration: GfiConfiguration,
features: (symbol | GeoJsonFeature<GeoJsonGeometry, GeoJsonProperties>[])[],
features: (symbol | GeoJsonFeature[])[],
srsName: string
): Record<string, GeoJsonFeature[] | symbol> => {
): FeaturesByLayerId => {
const generalMaxFeatures =
gfiConfiguration.maxFeatures || Number.POSITIVE_INFINITY
const featuresByLayerId = layerKeys.reduce(
Expand Down Expand Up @@ -56,7 +60,7 @@ const getPromisedFeatures = (
map: Map,
configuration: MapConfig,
layerKeys: string[],
coordinate: [number, number]
coordinateOrExtent: [number, number] | [number, number, number, number]
) =>
layerKeys.map((key) => {
const layer = map
Expand All @@ -82,27 +86,47 @@ const getPromisedFeatures = (
return requestGfi({
map,
layer,
coordinate,
coordinateOrExtent,
layerConfiguration,
layerSpecification,
mode: layerGfiMode,
})
})

const filterFeatures = (
featuresByLayerId: Record<
string,
symbol | GeoJsonFeature<GeoJsonGeometry, GeoJsonProperties>[]
>
): Record<string, GeoJsonFeature<GeoJsonGeometry, GeoJsonProperties>[]> => {
featuresByLayerId: FeaturesByLayerId
): Record<string, GeoJsonFeature[]> => {
const entries = Object.entries(featuresByLayerId)
const filtered = entries.filter((keyValue) => Array.isArray(keyValue[1])) as [
string,
GeoJsonFeature<GeoJsonGeometry, GeoJsonProperties>[]
GeoJsonFeature[]
][]
return Object.fromEntries(filtered)
}

const createSelectionDiff = (
oldSelection: FeaturesByLayerId,
newSelection: FeaturesByLayerId
): FeaturesByLayerId =>
Object.entries(newSelection).reduce(
(acc, [layerId, features]) => ({
...acc,
[layerId]:
Array.isArray(features) && Array.isArray(oldSelection[layerId])
? features.reduce((acc, newFeature) => {
// If the feature is already in the old selection, remove it
const oldFeatureIndex = acc.findIndex((oldFeature) =>
compare(oldFeature.properties, newFeature.properties)
)
return oldFeatureIndex === -1
? [...acc, newFeature]
: acc.filter((_, i) => i !== oldFeatureIndex)
}, oldSelection[layerId] as GeoJsonFeature[])
: features,
}),
{}
)

const errorSymbol = (err) => Symbol(err)

/**
Expand All @@ -117,31 +141,26 @@ const gfiRequest =
async (
{
commit,
getters: { layerKeys },
getters,
rootGetters: { map, configuration },
getters: { geometryLayerKeys, afterLoadFunction },
}: PolarActionContext<GfiState, GfiGetters>,
coordinate: [number, number]
{ coordinateOrExtent, modifierPressed = false }: GetFeatureInfoParameters
): Promise<void> => {
const { afterLoadFunction, layerKeys } = getters
// fetch new feature information for all configured layers
const promisedFeatures = getPromisedFeatures(
map,
configuration,
layerKeys,
coordinate
)
const features = (await Promise.allSettled(promisedFeatures)).map(
(result) =>
result.status === 'fulfilled'
? result.value
: errorSymbol(result.reason.message)
const features = (
await Promise.allSettled(
getPromisedFeatures(map, configuration, layerKeys, coordinateOrExtent)
)
).map((result) =>
result.status === 'fulfilled'
? result.value
: errorSymbol(result.reason.message)
)
const srsName: string = map.getView().getProjection().getCode()
let featuresByLayerId = filterAndMapFeaturesToLayerIds(
layerKeys,
// NOTE if there was no configuration, we would not be here
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
configuration.gfi!,
getters.gfiConfiguration,
features,
srsName
)
Expand All @@ -152,9 +171,15 @@ const gfiRequest =
srsName
)
}
if (modifierPressed) {
featuresByLayerId = createSelectionDiff(
getters.featureInformation,
featuresByLayerId
)
}
commit('setFeatureInformation', featuresByLayerId)
// render feature geometries to help layer
geometryLayerKeys
getters.geometryLayerKeys
.filter((key) => Array.isArray(featuresByLayerId[key]))
.forEach((key) =>
filterFeatures(featuresByLayerId)[key].forEach((feature) =>
Expand Down
42 changes: 34 additions & 8 deletions packages/plugins/Gfi/src/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import debounce from 'lodash.debounce'
import { Coordinate } from 'ol/coordinate'
import { Style, Fill, Stroke } from 'ol/style'
import Overlay from 'ol/Overlay'
import { GeoJSON } from 'ol/format'
Expand All @@ -8,6 +7,8 @@ import { Feature as GeoJsonFeature, GeoJsonProperties } from 'geojson'
import { PolarActionTree } from '@polar/lib-custom-types'
import getCluster from '@polar/lib-get-cluster'
import { getTooltip, Tooltip } from '@polar/lib-tooltip'
import { DragBox } from 'ol/interaction'
import { platformModifierKeyOnly } from 'ol/events/condition'
import { getFeatureDisplayLayer, clear } from '../../utils/displayFeatureLayer'
import { GfiGetters, GfiState } from '../../types'
import { getOriginalFeature } from '../../utils/getOriginalFeature'
Expand Down Expand Up @@ -40,7 +41,7 @@ export const makeActions = () => {
const reaction = (coordinate) => {
clear(featureDisplayLayer)
if (coordinate && coordinate.length) {
dispatch('getFeatureInfo', coordinate)
dispatch('getFeatureInfo', { coordinateOrExtent: coordinate })
}
}

Expand All @@ -64,6 +65,7 @@ export const makeActions = () => {
dispatch('setupFeatureVisibilityUpdates')
dispatch('setupCoreListener')
dispatch('setupZoomListeners')
dispatch('setupMultiSelection')
},
setupCoreListener({
getters: { gfiConfiguration },
Expand All @@ -78,6 +80,29 @@ export const makeActions = () => {
)
}
},
setupMultiSelection({ dispatch, getters, rootGetters }) {
if (getters.gfiConfiguration.boxSelect) {
const dragBox = new DragBox({ condition: platformModifierKeyOnly })
dragBox.on('boxend', () =>
dispatch('getFeatureInfo', {
coordinateOrExtent: dragBox.getGeometry().getExtent(),
modifierPressed: true,
})
)
rootGetters.map.addInteraction(dragBox)
}
if (getters.gfiConfiguration.directSelect) {
rootGetters.map.on('click', ({ coordinate, originalEvent }) =>
dispatch('getFeatureInfo', {
coordinateOrExtent: coordinate,
modifierPressed:
navigator.userAgent.indexOf('Mac') !== -1
? originalEvent.metaKey
: originalEvent.ctrlKey,
})
)
}
},
setupZoomListeners({ dispatch, getters, rootGetters }) {
if (getters.gfiConfiguration.featureList) {
this.watch(
Expand Down Expand Up @@ -177,12 +202,11 @@ export const makeActions = () => {
),
10
)
const usedLayers = Object.keys(getters.gfiConfiguration.layers)
rootGetters.map
.getLayers()
.getArray()
.forEach((layer) => {
if (usedLayers.includes(layer.get('id'))) {
if (getters.layerKeys.includes(layer.get('id'))) {
layer
// @ts-expect-error | layers reaching this have a source
.getSource()
Expand Down Expand Up @@ -211,13 +235,15 @@ export const makeActions = () => {
*/
async getFeatureInfo(
{ commit, dispatch },
coordinate: Coordinate
coordinateOrExtent: [number, number] | [number, number, number, number]
): Promise<GeoJsonFeature[]> {
commit('clearFeatureInformation')
commit('setVisibleWindowFeatureIndex', 0)
if (coordinateOrExtent.length === 2) {
commit('clearFeatureInformation')
commit('setVisibleWindowFeatureIndex', 0)
}
clear(featureDisplayLayer)
// call further stepped in a debounced fashion to avoid a mess
return await dispatch('debouncedGfiRequest', coordinate)
return await dispatch('debouncedGfiRequest', coordinateOrExtent)
},
debouncedGfiRequest: debouncedGfiRequest(featureDisplayLayer),
setCoreSelection(
Expand Down
Loading

0 comments on commit 3e37965

Please sign in to comment.