Skip to content

Commit

Permalink
Merge pull request #207 from Dataport/feature/gfi-set-feature-informa…
Browse files Browse the repository at this point in the history
…tion

Feature/gfi set feature information
  • Loading branch information
dopenguin authored Jan 10, 2025
2 parents dc27c41 + 397c57f commit 672dd30
Show file tree
Hide file tree
Showing 13 changed files with 1,140 additions and 156 deletions.
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

0 comments on commit 672dd30

Please sign in to comment.