Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0da5c97
Add documentation for soon to be added action setFeatureInformation
dopenguin Dec 4, 2024
efebbc0
Move action setupTooltip to a separate file
dopenguin Dec 4, 2024
cd273f8
Move action setupCoreListener to setup action file
dopenguin Dec 4, 2024
74fcb88
Move action setupMultiSelection to setup action file
dopenguin Dec 4, 2024
4fad81d
Move action setupZoomListeners to setup action file
dopenguin Dec 4, 2024
1c0f8b1
Move type FeatureByLayerId to central file
dopenguin Dec 4, 2024
54f8ebd
Move filterFeatures to a separate file to be a util function
dopenguin Dec 4, 2024
6f82b9b
Add information regarding isSelectable and the override
dopenguin Dec 4, 2024
16e9008
Add information on how to reset the selection
dopenguin Dec 4, 2024
4bf3c58
Move part of a function to be a new utility function renderFeatures
dopenguin Dec 4, 2024
682d639
Do not filter the features anew in each iteration
dopenguin Dec 4, 2024
3048229
Remove unused import
dopenguin Dec 4, 2024
7d24515
Add setFeatureInformation to be able to programmatically add features
dopenguin Dec 4, 2024
55e73bb
Add an example for the new action setFeatureInformation to the snowbox
dopenguin Dec 4, 2024
125215f
Add tests for util functions filterFeatures and sortFeatures
dopenguin Dec 5, 2024
a4b044a
Merge branch 'main' into feature/gfi-set-feature-information
dopenguin Dec 6, 2024
d679903
Merge branch 'main' into feature/gfi-set-feature-information
dopenguin Dec 11, 2024
b8a4dae
Merge branch 'main' into feature/gfi-set-feature-information
dopenguin Dec 26, 2024
7936711
Merge branch 'main' into feature/gfi-set-feature-information
dopenguin Jan 4, 2025
0f386c6
Merge branch 'main' into feature/gfi-set-feature-information
dopenguin Jan 6, 2025
4937238
Merge branch 'main' into feature/gfi-set-feature-information
warm-coolguy Jan 6, 2025
2149091
Fix e2e tests breaking by moving the click handler outside createMap
dopenguin Jan 6, 2025
9afba0c
Adjust documentation of setFeatureInformation
dopenguin Jan 6, 2025
397c57f
Remove unnecessary property
dopenguin Jan 6, 2025
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
538 changes: 538 additions & 0 deletions packages/clients/snowbox/src/exampleFeatureInformation.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/clients/snowbox/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ <h2>🗺️ Map</h2>
<noscript>Please use a browser with active JavaScript to use the map client.</noscript>
</div>
<h2>Example for programmatic information binding</h2>
<button id="vuex-target-clicky">Click here to programmatically select some features</button>
<p>
This illustrates which kind of data can be retrieved from the map client.
</p>
Expand Down
9 changes: 9 additions & 0 deletions packages/clients/snowbox/src/polar-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { changeLanguage } from 'i18next'
import { enableClustering } from '../../meldemichel/src/utils/enableClustering'
import { addPlugins } from './addPlugins'
import { mapConfiguration, reports } from './mapConfiguration'
import { exampleFeatureInformation } from './exampleFeatureInformation'

addPlugins(polarCore)

Expand Down Expand Up @@ -80,3 +81,11 @@ document
target[1].innerHTML = value === 'en' ? 'German' : 'Deutsch'
})
})

document.getElementById('vuex-target-clicky')!.addEventListener('click', () =>
// @ts-expect-error | added for e2e testing
window.mapInstance.$store.dispatch(
'plugin/gfi/setFeatureInformation',
exampleFeatureInformation
)
)
4 changes: 4 additions & 0 deletions packages/plugins/Gfi/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## unpublished

- Feature: Add new action `setFeatureInformation` to be able to set feature information in the store and trigger all relevant processes so that the information displayed to the user is as if he has selected the features himself.

## 2.0.0

- Breaking: Upgrade `@masterportal/masterportalapi` from `2.8.0` to `2.40.0` and subsequently `ol` from `^7.1.0` to `^9.2.4`.
Expand Down
50 changes: 50 additions & 0 deletions packages/plugins/Gfi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,56 @@ featureList: {

## Store

### Actions

#### setFeatureInformation

This method can be used to set the feature information in the store and trigger all relevant processes so that the information displayed to the user is as if he has selected the features himself.
Note that calling this method completely overrides the previously set feature information.

If a layer has a `isSelectable`-function configured, the features are filtered using that function.

```js
map.$store.dispatch('plugin/gfi/setFeatureInformation', {
"anotherLayer": [],
"yetAnotherLayer": [],
"relevantInformation": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
565669.6521397199,
5930516.358614317
]
},
"properties": {
"propertyOne": "B0",
"propertyTwo": "B1"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
565594.9377660984,
5930524.52634174
]
},
"properties": {
propertyOne: "A0",
propertyTwo: "A1"
}
}
]
})
```

The payload object expects a layer id as a key and an array of GeoJSON-Features as its value.

The selected feature information can be reset by calling the method with an empty object.

### State

If a successful query has been sent and a response has been received, the result will be saved in the store and can be subscribed through the path `'plugin/gfi/featureInformation'`. If, however, a query for a layer fails, a `Symbol` containing the error will be saved in the store instead to indicate the error.
Expand Down
31 changes: 8 additions & 23 deletions packages/plugins/Gfi/src/store/actions/debouncedGfiRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,17 @@ 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 { filterFeatures } from '../../utils/filterFeatures'
import { requestGfi } from '../../utils/requestGfi'
import sortFeatures from '../../utils/sortFeatures'
import { GfiGetters, GfiState } from '../../types'
import { FeaturesByLayerId, GfiGetters, GfiState } from '../../types'
import { renderFeatures } from '../../utils/renderFeatures'

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

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

const filterAndMapFeaturesToLayerIds = (
layerKeys: string[],
gfiConfiguration: GfiConfiguration,
Expand Down Expand Up @@ -93,17 +92,6 @@ const getPromisedFeatures = (
})
})

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

const createSelectionDiff = (
oldSelection: FeaturesByLayerId,
newSelection: FeaturesByLayerId
Expand Down Expand Up @@ -178,14 +166,11 @@ const gfiRequest =
)
}
commit('setFeatureInformation', featuresByLayerId)
// render feature geometries to help layer
getters.geometryLayerKeys
.filter((key) => Array.isArray(featuresByLayerId[key]))
.forEach((key) =>
filterFeatures(featuresByLayerId)[key].forEach((feature) =>
addFeature(feature, featureDisplayLayer)
)
)
renderFeatures(
featureDisplayLayer,
getters.geometryLayerKeys,
featuresByLayerId
)
}

export const debouncedGfiRequest = (
Expand Down
175 changes: 42 additions & 133 deletions packages/plugins/Gfi/src/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import debounce from 'lodash.debounce'
import { Feature as GeoJsonFeature } from 'geojson'
import { Style, Fill, Stroke } from 'ol/style'
import Overlay from 'ol/Overlay'
import { GeoJSON } from 'ol/format'
import { Feature } from 'ol'
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'
import { FeaturesByLayerId, GfiGetters, GfiState } from '../../types'
import { filterFeatures } from '../../utils/filterFeatures'
import { renderFeatures } from '../../utils/renderFeatures'
import { debouncedGfiRequest } from './debouncedGfiRequest'
import {
setupCoreListener,
setupMultiSelection,
setupTooltip,
setupZoomListeners,
} from './setup'

// OK for module action set creation
// eslint-disable-next-line max-lines-per-function
Expand Down Expand Up @@ -67,131 +69,10 @@ export const makeActions = () => {
dispatch('setupZoomListeners')
dispatch('setupMultiSelection')
},
setupCoreListener({
getters: { gfiConfiguration },
rootGetters,
dispatch,
}) {
if (gfiConfiguration.featureList?.bindWithCoreHoverSelect) {
this.watch(
() => rootGetters.selected,
(feature) => dispatch('setOlFeatureInformation', { feature }),
{ deep: true }
)
}
},
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(
() => rootGetters.zoomLevel,
() => {
const {
featureInformation,
listableLayerSources,
visibleWindowFeatureIndex,
windowFeatures,
} = getters

if (windowFeatures.length) {
const layerId: string =
// @ts-expect-error | if windowFeatures has features, visibleWindowFeatureIndex is in the range of possible features
windowFeatures[visibleWindowFeatureIndex].polarInternalLayerKey
const selectedFeatureProperties: GeoJsonProperties = {
// eslint-disable-next-line @typescript-eslint/naming-convention
_gfiLayerId: layerId,
...featureInformation[layerId][visibleWindowFeatureIndex]
.properties,
}
const originalFeature = getOriginalFeature(
listableLayerSources,
selectedFeatureProperties
)
if (originalFeature) {
dispatch('setOlFeatureInformation', {
feature: getCluster(
rootGetters.map,
originalFeature,
'_gfiLayerId'
),
})
}
}
}
)
}
},
setupTooltip({ getters: { gfiConfiguration }, rootGetters: { map } }) {
const tooltipLayerIds = Object.keys(gfiConfiguration.layers).filter(
(key) => gfiConfiguration.layers[key].showTooltip
)
if (!tooltipLayerIds.length) {
return
}

let element: Tooltip['element'], unregister: Tooltip['unregister']
const overlay = new Overlay({
positioning: 'bottom-center',
offset: [0, -5],
})
map.addOverlay(overlay)
map.on('pointermove', ({ pixel, dragging, originalEvent }) => {
if (dragging || ['touch', 'pen'].includes(originalEvent.pointerType)) {
return
}
let hasFeatureAtPixel = false
// stops on return `true`, thus only using the uppermost feature
map.forEachFeatureAtPixel(
pixel,
(feature, layer) => {
if (!(feature instanceof Feature)) {
return false
}
hasFeatureAtPixel = true
overlay.setPosition(map.getCoordinateFromPixel(pixel))
if (unregister) {
unregister()
}
;({ element, unregister } = getTooltip({
localeKeys:
// @ts-expect-error | it exists by virtue of layerFilter below
gfiConfiguration.layers[layer.get('id')].showTooltip(
feature,
map
),
}))
overlay.setElement(element)
return true
},
{ layerFilter: (layer) => tooltipLayerIds.includes(layer.get('id')) }
)
if (!hasFeatureAtPixel) {
overlay.setPosition(undefined)
}
})
},
setupCoreListener,
setupMultiSelection,
setupTooltip,
setupZoomListeners,
setupFeatureVisibilityUpdates({ commit, state, getters, rootGetters }) {
// debounce to prevent update spam
debouncedVisibilityChangeIndicator = debounce(
Expand Down Expand Up @@ -283,6 +164,34 @@ export const makeActions = () => {
dispatch('setCoreSelection', { feature, centerOnFeature })
}
},
setFeatureInformation(
{ commit, getters },
featuresByLayerId: FeaturesByLayerId
) {
commit('clearFeatureInformation')
commit('setVisibleWindowFeatureIndex', 0)
clear(featureDisplayLayer)

const filteredFeatures = Object.fromEntries(
Object.entries(filterFeatures(featuresByLayerId)).map(
([layerId, features]) => {
const { isSelectable } = getters.gfiConfiguration.layers[layerId]
return [
layerId,
typeof isSelectable === 'function'
? features.filter((feature) => isSelectable(feature))
: features,
]
}
)
)
commit('setFeatureInformation', filteredFeatures)
renderFeatures(
featureDisplayLayer,
getters.geometryLayerKeys,
filteredFeatures
)
},
hover({ commit, rootGetters }, feature: Feature) {
if (rootGetters.configuration.extendedMasterportalapiMarkers) {
commit('setHovered', feature, { root: true })
Expand Down
Loading
Loading