diff --git a/packages/plugins/Gfi/src/store/actions/debouncedGfiRequest.ts b/packages/plugins/Gfi/src/store/actions/debouncedGfiRequest.ts new file mode 100644 index 000000000..06af3d1b0 --- /dev/null +++ b/packages/plugins/Gfi/src/store/actions/debouncedGfiRequest.ts @@ -0,0 +1,166 @@ +import debounce from 'lodash.debounce' +import { rawLayerList } from '@masterportal/masterportalapi' +import { + Feature as GeoJsonFeature, + GeoJsonProperties, + Geometry as GeoJsonGeometry, +} from 'geojson' +import { + GfiConfiguration, + MapConfig, + PolarActionContext, +} from '@polar/lib-custom-types' +import { Map, Feature } from 'ol' +import { Geometry } from 'ol/geom' +import VectorLayer from 'ol/layer/Vector' +import { addFeature } from '../../utils/displayFeatureLayer' +import { requestGfi } from '../../utils/requestGfi' +import sortFeatures from '../../utils/sortFeatures' +import { GfiGetters, GfiState } from '../../types' + +const mapFeaturesToLayerIds = ( + layerKeys: string[], + gfiConfiguration: GfiConfiguration, + features: (symbol | GeoJsonFeature[])[], + srsName: string +): Record => { + const generalMaxFeatures = + gfiConfiguration.maxFeatures || Number.POSITIVE_INFINITY + const featuresByLayerId = layerKeys.reduce( + (accumulator, key, index) => ({ + ...accumulator, + [key]: Array.isArray(features[index]) + ? (features[index] as []).slice( + 0, + gfiConfiguration.layers[key].maxFeatures || generalMaxFeatures + ) + : features[index], + }), + {} + ) + return Object.entries(featuresByLayerId).reduce( + (accumulator, [layerKey, layerValues]) => ({ + ...accumulator, + [layerKey]: + Array.isArray(layerValues) && layerValues.length >= 2 + ? layerValues.sort((a, b) => sortFeatures(a, b, srsName)) + : layerValues, + }), + {} + ) +} + +const getPromisedFeatures = ( + map: Map, + configuration: MapConfig, + layerKeys: string[], + coordinate: [number, number] +) => + layerKeys.map((key) => { + const layer = map + .getLayers() + .getArray() + .find((layer) => layer.getProperties().id === key) + + if (!layer) { + console.error( + `@polar/plugin-gfi: No layer with id "${key}" found during run-time. GFI skipped.` + ) + return [] as GeoJsonFeature[] + } + + const layerConfiguration = configuration.gfi?.layers[key] || {} + const layerSpecification = rawLayerList.getLayerWhere({ id: key }) + const mainLayerConfiguration = configuration.layers.find( + (element) => element.id === key + ) + const layerGfiMode = + mainLayerConfiguration?.gfiMode || configuration?.gfi?.mode || 'bboxDot' + + return requestGfi({ + map, + layer, + coordinate, + layerConfiguration, + layerSpecification, + mode: layerGfiMode, + }) + }) + +const filterFeatures = ( + featuresByLayerId: Record< + string, + symbol | GeoJsonFeature[] + > +): Record[]> => { + const entries = Object.entries(featuresByLayerId) + const filtered = entries.filter((keyValue) => Array.isArray(keyValue[1])) as [ + string, + GeoJsonFeature[] + ][] + return Object.fromEntries(filtered) +} + +const errorSymbol = (err) => Symbol(err) + +/** + * Code from `getFeatureInfo`, pulled to avoid overly requesting feature + * information. Since sources in Pins plugin update right after each other + * (and such effects are to be expected across the system), we're debouncing + * this *after* resetting the module state, as something is bound to happen. + */ +// eslint-disable-next-line max-lines-per-function +const gfiRequest = + (featureDisplayLayer: VectorLayer>) => + async ( + { + commit, + getters: { layerKeys }, + rootGetters: { map, configuration }, + getters: { geometryLayerKeys, afterLoadFunction }, + }: PolarActionContext, + coordinate: [number, number] + ): Promise => { + // 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 srsName: string = map.getView().getProjection().getCode() + let featuresByLayerId = mapFeaturesToLayerIds( + layerKeys, + // NOTE if there was no configuration, we would not be here + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + configuration.gfi!, + features, + srsName + ) + // store features in state, if configured via client after specific function + if (typeof afterLoadFunction === 'function') { + featuresByLayerId = await afterLoadFunction( + filterFeatures(featuresByLayerId), + srsName + ) + } + commit('setFeatureInformation', featuresByLayerId) + // render feature geometries to help layer + geometryLayerKeys + .filter((key) => Array.isArray(featuresByLayerId[key])) + .forEach((key) => + filterFeatures(featuresByLayerId)[key].forEach((feature) => + addFeature(feature, featureDisplayLayer) + ) + ) + } + +export const debouncedGfiRequest = ( + featureDisplayLayer: VectorLayer> +) => debounce(gfiRequest(featureDisplayLayer), 50) diff --git a/packages/plugins/Gfi/src/store/actions/index.ts b/packages/plugins/Gfi/src/store/actions/index.ts index 2b0706538..7a284ccdd 100644 --- a/packages/plugins/Gfi/src/store/actions/index.ts +++ b/packages/plugins/Gfi/src/store/actions/index.ts @@ -1,24 +1,17 @@ import debounce from 'lodash.debounce' -import compare from 'just-compare' import { Coordinate } from 'ol/coordinate' 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 { rawLayerList } from '@masterportal/masterportalapi' import { PolarActionTree } from '@polar/lib-custom-types' import getCluster from '@polar/lib-get-cluster' -import { isVisible } from '@polar/lib-invisible-style' import { getTooltip, Tooltip } from '@polar/lib-tooltip' -import { - getFeatureDisplayLayer, - clear, - addFeature, -} from '../../utils/displayFeatureLayer' -import { requestGfi } from '../../utils/requestGfi' +import { getFeatureDisplayLayer, clear } from '../../utils/displayFeatureLayer' import { GfiGetters, GfiState } from '../../types' -import sortFeatures from '../../utils/sortFeatures' +import { getOriginalFeature } from '../../utils/getOriginalFeature' +import { debouncedGfiRequest } from './debouncedGfiRequest' // OK for module action set creation // eslint-disable-next-line max-lines-per-function @@ -107,28 +100,10 @@ export const makeActions = () => { ...featureInformation[layerId][visibleWindowFeatureIndex] .properties, } - const originalFeature = listableLayerSources - .map((source) => - source - .getFeatures() - .filter(isVisible) - .map((feature) => { - // true = silent change (prevents cluster recomputation & rerender) - feature.set( - '_gfiLayerId', - source.get('_gfiLayerId'), - true - ) - return feature - }) - ) - .flat(1) - .find((f) => - compare( - JSON.parse(new GeoJSON().writeFeature(f)).properties, - selectedFeatureProperties - ) - ) + const originalFeature = getOriginalFeature( + listableLayerSources, + selectedFeatureProperties + ) if (originalFeature) { dispatch('setOlFeatureInformation', { feature: getCluster( @@ -244,117 +219,7 @@ export const makeActions = () => { // call further stepped in a debounced fashion to avoid a mess return await dispatch('debouncedGfiRequest', coordinate) }, - /** - * Code from `getFeatureInfo`, pulled to avoid overly requesting feature - * information. Since sources in Pins plugin update right after each other - * (and such effects are to be expected across the system), we're debouncing - * this *after* resetting the module state, as something is bound to happen. - */ - debouncedGfiRequest: debounce( - // TODO: Types are not properly displayed here as it is wrapped through debounce - async ( - { - commit, - rootGetters: { map, configuration }, - getters: { layerKeys, geometryLayerKeys, afterLoadFunction }, - }, - coordinate: [number, number] - ): Promise => { - // fetch new feature information for all configured layers - const promisedFeatures: Promise[] = layerKeys.map( - (key) => { - const layer = map - .getLayers() - .getArray() - .find((layer) => layer.getProperties().id === key) - - if (!layer) { - console.error( - `@polar/plugin-gfi: No layer with id "${key}" found during run-time. GFI skipped.` - ) - return [] as GeoJsonFeature[] - } - - const layerConfiguration = configuration.gfi.layers[key] || {} - const layerSpecification = rawLayerList.getLayerWhere({ id: key }) - const mainLayerConfiguration = configuration.layers.find( - (element) => element.id === key - ) - const layerGfiMode = - mainLayerConfiguration.gfiMode || - configuration.gfi.mode || - 'bboxDot' - - return requestGfi({ - map, - layer, - coordinate, - layerConfiguration, - layerSpecification, - mode: layerGfiMode, - }) - } - ) - - const errorSymbol = (err) => Symbol(err) - const features = (await Promise.allSettled(promisedFeatures)).map( - (result) => - result.status === 'fulfilled' - ? result.value - : errorSymbol(result.reason.message) - ) - - const generalMaxFeatures: number = - configuration.gfi.maxFeatures || Number.POSITIVE_INFINITY - - const srsName: string = map.getView().getProjection().getCode() - // map features back to their layer keys - let featuresByLayerId: Record = - layerKeys.reduce( - (accumulator, key, index) => ({ - ...accumulator, - [key]: Array.isArray(features[index]) - ? (features[index] as []).slice( - 0, - configuration.gfi.layers[key].maxFeatures || - generalMaxFeatures - ) - : features[index], - }), - {} - ) - featuresByLayerId = Object.entries(featuresByLayerId).reduce( - (accumulator, [layerKey, layerValues]) => ({ - ...accumulator, - [layerKey]: - Array.isArray(layerValues) && layerValues.length >= 2 - ? layerValues.sort((a, b) => sortFeatures(a, b, srsName)) - : layerValues, - }), - {} - ) - - // store features in state, if configured via client specific function - if (typeof afterLoadFunction === 'function') { - featuresByLayerId = await afterLoadFunction( - featuresByLayerId, - srsName - ) - } - commit('setFeatureInformation', featuresByLayerId) - - // render feature geometries to help layer - geometryLayerKeys - .filter((key) => Array.isArray(featuresByLayerId[key])) - .forEach((key) => - // @ts-expect-error | Might be fixed through having all the types in the action. Otherwise: It works properly, as all the symbols are filtered before calling forEach - featuresByLayerId[key].forEach((feature) => - addFeature(feature, featureDisplayLayer) - ) - ) - }, - 50 - ), + debouncedGfiRequest: debouncedGfiRequest(featureDisplayLayer), setCoreSelection( { commit, dispatch, rootGetters }, { diff --git a/packages/plugins/Gfi/src/utils/getOriginalFeature.ts b/packages/plugins/Gfi/src/utils/getOriginalFeature.ts new file mode 100644 index 000000000..73d36750e --- /dev/null +++ b/packages/plugins/Gfi/src/utils/getOriginalFeature.ts @@ -0,0 +1,27 @@ +import { GeoJSON } from 'ol/format' +import { Geometry } from 'ol/geom' +import { Feature } from 'ol' +import compare from 'just-compare' +import { isVisible } from '@polar/lib-invisible-style' +import { GeoJsonProperties } from 'geojson' +import VectorSource from 'ol/source/Vector' + +export const getOriginalFeature = ( + sources: VectorSource>[], + properties: GeoJsonProperties +) => + sources + .map((source) => + source + .getFeatures() + .filter(isVisible) + .map((feature) => { + // true = silent change (prevents cluster recomputation & rerender) + feature.set('_gfiLayerId', source.get('_gfiLayerId'), true) + return feature + }) + ) + .flat(1) + .find((f) => + compare(JSON.parse(new GeoJSON().writeFeature(f)).properties, properties) + ) diff --git a/packages/plugins/Gfi/src/utils/requestGfiWfs.ts b/packages/plugins/Gfi/src/utils/requestGfiWfs.ts index b88d4d12a..1e4b18d4b 100644 --- a/packages/plugins/Gfi/src/utils/requestGfiWfs.ts +++ b/packages/plugins/Gfi/src/utils/requestGfiWfs.ts @@ -30,6 +30,8 @@ const xml2GeoJson = ( * no new request should be started. Instead, wait for any running load to * finish and use feature at layer source coordinate. (Not trivial.) */ +// NOTE length comes from string build, not complexity – keep as it is +// eslint-disable-next-line max-lines-per-function export default ({ map, coordinate, diff --git a/packages/plugins/Gfi/src/utils/requestGfiWms.ts b/packages/plugins/Gfi/src/utils/requestGfiWms.ts index a36a225b0..addad5ff8 100644 --- a/packages/plugins/Gfi/src/utils/requestGfiWms.ts +++ b/packages/plugins/Gfi/src/utils/requestGfiWms.ts @@ -31,7 +31,6 @@ function readTextFeatures(text: string): Feature[] { const lines = text.split('\n') const features: Feature[] = [] let feature: Feature | undefined - /* TODO: Format supposedly looks like this – is this a standard or arbitrary? GetFeatureInfo results: LayerName: @@ -76,7 +75,6 @@ function readTextFeatures(text: string): Feature[] { features.push(feature) } } - return features } diff --git a/packages/types/custom/CHANGELOG.md b/packages/types/custom/CHANGELOG.md index 656528e33..987e60e50 100644 --- a/packages/types/custom/CHANGELOG.md +++ b/packages/types/custom/CHANGELOG.md @@ -11,6 +11,7 @@ - Feature: Add new parameter `showZoomSlider` to `ZoomConfiguration`. - Feature: Add new types `LoaderStyles` and `LoadingIndicatorConfiguration`. - Feature: Add `loadingIndicator` to `MapConfig` to configure loader style. +- Fix: Document missing return type to `afterLoadFunction`, which may also return a Promise. - Fix: Add `string` as option for `SearchType` since arbitrary strings can be registered. - Fix: Remove unused parameters `proxyUrl` and `loadingStrategy` from `LayerConfigurationOptions`. - Fix: Properly document optional parameters of interfaces `AddressSearchConfiguration`, `FeatureList`, `FilterConfigurationTime`, `FilterConfigurationTimeOption`, `GeoLocationConfiguration`, `LayerConfigurationOptionLayers` and `PinsConfiguration` @@ -18,6 +19,8 @@ - Fix: Remove mpapi-search specific parameters from general interface `QueryParameters`. - Fix: Extend `SelectResultPayload` with fitting vuex parameters, and `SelectResultFunction` with `title` field as used in `@polar/plugin-address-search`. - Fix: Use correct type `VueConstructor` for properties `GfiConfiguration.gfiContentComponent`, `MoveHandleProperties.component` and `MoveHandleActionButton.component`. +- Fix: Add missing `gfiMode` to `LayerConfiguration`. +- Fix: Add missing `maxFeatures` to `GfiLayerConfiguration`. ## 1.4.1 diff --git a/packages/types/custom/core.ts b/packages/types/custom/core.ts index fc9deaadf..012bd9120 100644 --- a/packages/types/custom/core.ts +++ b/packages/types/custom/core.ts @@ -250,6 +250,7 @@ export interface GfiLayerConfiguration { geometry?: boolean // name of field to use for geometry, if not default field geometryName?: string + maxFeatures?: number /** * If window is true, the properties are either * 1. filtered by whether their key is in a string[] @@ -324,7 +325,9 @@ export interface FullscreenConfiguration extends PluginOptions { export type GfiAfterLoadFunction = ( featureInformation: Record, srsName: string // TODO: Might be interesting to overlap this with mapConfig.namedProjections for type safety in using only allowed epsg codes -) => Record +) => + | Record + | Promise> /** GFI Module Configuration */ export interface FeatureList { @@ -533,6 +536,8 @@ export interface LayerConfiguration { options?: LayerConfigurationOptions /** Whether the layer should be rendered; defaults to false */ visibility?: boolean + /** layers may have their own gfiMode */ + gfiMode?: 'bboxDot' | 'intersects' } export interface PolarMapOptions {