diff --git a/examples/generic/index.js b/examples/generic/index.js index cb3edfba1d..74060a739a 100644 --- a/examples/generic/index.js +++ b/examples/generic/index.js @@ -11,15 +11,7 @@ let colorScheme = 'light' // arbitrary condition for testing const isEvenId = (mmlid) => Number(mmlid.slice(-1)) % 2 === 0 - -// NOTE: This function is only usable if the layer is clustered -const isReportSelectable = (feature) => - feature - .get('features') - .reduce( - (accumulator, current) => isEvenId(current.get('mmlid')) || accumulator, - false - ) +const isReportSelectable = (feature) => isEvenId(feature.get('mmlid')) const map = await createMap( 'polarstern', diff --git a/examples/github-io/components/HeroPolarMap.vue b/examples/github-io/components/HeroPolarMap.vue index 7ee52c1526..2a0067a995 100644 --- a/examples/github-io/components/HeroPolarMap.vue +++ b/examples/github-io/components/HeroPolarMap.vue @@ -7,7 +7,6 @@ diff --git a/src/plugins/gfi/composables/useBindWithCoreHoverSelect.ts b/src/plugins/gfi/composables/useBindWithCoreHoverSelect.ts index cc0973d220..c2e0883837 100644 --- a/src/plugins/gfi/composables/useBindWithCoreHoverSelect.ts +++ b/src/plugins/gfi/composables/useBindWithCoreHoverSelect.ts @@ -1,108 +1,103 @@ import type { Feature } from 'ol' import type { ShallowRef } from 'vue' -import ClusterSource from 'ol/source/Cluster' import { markRaw, watch } from 'vue' -import { useCoreStore } from '@/core/stores' -import getCluster from '@/lib/getCluster' - import { useGfiMainStore } from '../stores/main' import { filterSelectableFeatures } from '../utils/filterSelectableFeatures' type CoreFeature = Feature | null -type GfiFeature = Record - -function assignCoreToGfi(target: ShallowRef, feature: CoreFeature) { +type CoreFeatures = Feature[] +type GfiFeature = { layerId: string; feature: Feature } | null +type GfiFeatures = Partial> + +function assignCoreToGfiCluster( + target: ShallowRef, + features: CoreFeatures, + referenceOrder?: CoreFeatures +) { const gfiMainStore = useGfiMainStore() - - if (feature === null) { - target.value = markRaw({}) - return + const layerId = features[0]?.get('_polarLayerId') + const layerConfiguration = gfiMainStore.getLayerConfiguration(layerId) + + if (referenceOrder) { + features.sort((a, b) => { + const getIndex = (feature) => referenceOrder.indexOf(feature) + return getIndex(a) - getIndex(b) + }) } - const layerId = feature.get('_polarLayerId') - const newFeatures = filterSelectableFeatures( - feature.get('features') || [feature], - gfiMainStore.getLayerConfiguration(layerId)?.isSelectable + target.value = markRaw( + Object.groupBy( + filterSelectableFeatures(features, layerConfiguration?.isSelectable), + (feature) => feature.get('_polarLayerId') + ) ) - target.value = markRaw({ - ...(newFeatures.length ? { [layerId]: markRaw(newFeatures) } : {}), - }) } -function assignGfiToCore( +function assignGfiToCoreFeature( target: ShallowRef, - featureMap: GfiFeature + feature: GfiFeature ) { - const coreStore = useCoreStore() - - const features = Object.entries(featureMap).flatMap(([layerId, features]) => - features.map((feature) => ({ layerId, feature })) - ) - - // The second condition is necessary for TypeScript checks. - if (features.length <= 0 || !features[0]) { + if (!feature) { target.value = null return } + target.value = markRaw(feature.feature) +} - const targetFeature = target.value - const targetLayerId = targetFeature?.get('_polarLayerId') - const targetClusterFeatures = - targetFeature?.get('features') || (targetFeature ? [targetFeature] : []) - if ( - targetLayerId === features[0].layerId && - targetClusterFeatures.length === features.length && - features.every(({ feature }) => targetClusterFeatures.includes(feature)) - ) { +function assignCoreToGfiFeature( + target: ShallowRef, + feature: CoreFeature +) { + if (!feature) { + target.value = null return } - - const { feature, layerId } = features[0] - feature.set('_polarLayerId', layerId, true) - target.value = markRaw( - coreStore.getLayer(layerId)?.getSource() instanceof ClusterSource - ? getCluster(coreStore.map, feature, '_polarLayerId') - : feature - ) + target.value = markRaw({ + layerId: feature.get('_polarLayerId'), + feature, + }) } -export function useBindWithCoreHoverSelect( - hoveredFeatures: ShallowRef>, - selectedFeatures: ShallowRef>, - coreHoveredFeature: ShallowRef, - coreSelectedFeature: ShallowRef +function bindWithWatcher( + target: ShallowRef, + source: ShallowRef, + handler: (target: ShallowRef, source: S, extra?: E) => void, + extra?: ShallowRef ) { watch( - coreHoveredFeature, - (feature) => { - assignCoreToGfi(hoveredFeatures, feature) - }, - { immediate: true } - ) - - watch( - coreSelectedFeature, - (feature) => { - assignCoreToGfi(selectedFeatures, feature) + source, + (value) => { + handler(target, value, extra?.value) }, - { immediate: true } + { + immediate: true, + } ) +} - watch( - hoveredFeatures, - (featureMap) => { - assignGfiToCore(coreHoveredFeature, featureMap) - }, - { immediate: true } - ) +export function useBindWithCoreHoverSelect( + hoveredFeature: ShallowRef, + coreHoveredFeature: ShallowRef, + hoveredFeatures: ShallowRef, + coreHoveredFeatures: ShallowRef, + selectedFeature: ShallowRef, + coreSelectedFeature: ShallowRef, + selectedFeatures: ShallowRef, + coreSelectedFeatures: ShallowRef, + referenceOrder: ShallowRef +) { + bindWithWatcher(hoveredFeature, coreHoveredFeature, assignCoreToGfiFeature) + bindWithWatcher(coreHoveredFeature, hoveredFeature, assignGfiToCoreFeature) + bindWithWatcher(hoveredFeatures, coreHoveredFeatures, assignCoreToGfiCluster) - watch( + bindWithWatcher(selectedFeature, coreSelectedFeature, assignCoreToGfiFeature) + bindWithWatcher(coreSelectedFeature, selectedFeature, assignGfiToCoreFeature) + bindWithWatcher( selectedFeatures, - (featureMap) => { - assignGfiToCore(coreSelectedFeature, featureMap) - }, - { immediate: true } + coreSelectedFeatures, + assignCoreToGfiCluster, + referenceOrder ) } diff --git a/src/plugins/gfi/composables/useSelectedFeatures.ts b/src/plugins/gfi/composables/useSelectedFeatures.ts index 66422da7ec..59976baa6b 100644 --- a/src/plugins/gfi/composables/useSelectedFeatures.ts +++ b/src/plugins/gfi/composables/useSelectedFeatures.ts @@ -1,28 +1,115 @@ import type { Feature as GeoJsonFeature } from 'geojson' -import type { Feature } from 'ol' +import type { Feature as OlFeature } from 'ol' +import { isEqual, mapValues } from 'es-toolkit' import { GeoJSON } from 'ol/format' -import { computed, ref, shallowRef } from 'vue' +import { computed, shallowRef } from 'vue' +interface Feature { + geojsonFeature: GeoJsonFeature + olFeature?: OlFeature +} + +/** + * The GFI plugin may be used either with a vector layer or using GFI requests. + * If a vector layer is used, we have OpenLayers features that are converted to GeoJSON features. + * Otherwise, the response are just GeoJSON features. + * + * This composable considers this and allows for both scenarios: + * For vector layers, both features are updated. + * For GFI requests, only the GeoJSON features are used. + */ export function useSelectedFeatures() { - const selectedFeatures = shallowRef>({}) - const featureInformation = ref>({}) + const features = shallowRef>({}) + const feature = shallowRef<(Feature & { layerId: string }) | null>(null) + + const olFeatures = computed>({ + get: () => + mapValues(features.value, (list) => + list + .filter( + (feature): feature is Feature & { olFeature: Feature } => + 'olFeature' in feature + ) + .map(({ olFeature }) => olFeature) + ), + set: (value) => { + features.value = mapValues(value, (list) => + list.map((olFeature) => ({ + olFeature, + geojsonFeature: new GeoJSON().writeFeatureObject(olFeature), + })) + ) + }, + }) + + const geojsonFeatures = computed>({ + get: () => + mapValues(features.value, (list) => + list.map(({ geojsonFeature }) => geojsonFeature) + ), + set: (value) => { + features.value = mapValues(value, (list) => + list.map((geojsonFeature) => ({ geojsonFeature })) + ) + }, + }) + + const olFeature = computed<{ + layerId: string + feature: OlFeature + } | null>({ + get: () => + feature.value?.olFeature + ? { + layerId: feature.value.layerId, + feature: feature.value.olFeature, + } + : null, + set: (value) => { + if (value === null) { + feature.value = null + return + } + const layerId = value.layerId + feature.value = { + layerId, + olFeature: value.feature, + geojsonFeature: new GeoJSON().writeFeatureObject(value.feature), + } + }, + }) + + const geojsonFeature = computed<{ + layerId: string + feature: GeoJsonFeature + } | null>({ + get: () => + feature.value + ? { + layerId: feature.value.layerId, + feature: feature.value.geojsonFeature, + } + : null, + set: (value) => { + if (value === null) { + feature.value = null + return + } + const layerId = value.layerId + const item = features.value[layerId]?.find(({ geojsonFeature }) => + isEqual(geojsonFeature, value.feature) + ) + feature.value = item + ? { ...item, layerId } + : { layerId, geojsonFeature: value.feature } + }, + }) return { - selectedFeatures: computed({ - get: () => selectedFeatures.value, - set: (value) => { - selectedFeatures.value = value - featureInformation.value = Object.fromEntries( - Object.entries(value).map(([layerId, features]) => [ - layerId, - features.map((feature) => - new GeoJSON().writeFeatureObject(feature) - ), - ]) - ) - }, - }), - featureInformation, + olFeatures, + olFeature, + geojsonFeatures, + geojsonFeature, } } diff --git a/src/plugins/gfi/store.ts b/src/plugins/gfi/store.ts index 4f7de3e0e7..b8457a7aec 100644 --- a/src/plugins/gfi/store.ts +++ b/src/plugins/gfi/store.ts @@ -5,6 +5,7 @@ /* eslint-enable tsdoc/syntax */ import { acceptHMRUpdate, defineStore, storeToRefs } from 'pinia' +import { readonly } from 'vue' import { useGfiFeatureStore } from './stores/feature' import { useGfiListStore } from './stores/list' @@ -43,35 +44,49 @@ export const useGfiStore = defineStore('plugins/gfi', () => { renderType: gfiMainStoreRefs.renderType, /** - * Hovered features. + * Hovered feature. + * This is a feature whose hovering state is originated at the plugin. + * * This is only used for the list view. * * @alpha */ - hoveredFeatures: gfiListStoreRefs.hoveredFeatures, + hoveredFeature: gfiListStoreRefs.hoveredFeature, /** - * Mark feature as hovered. + * Hovered features. + * The hovering state may be originated either from {@link hoveredFeature} or by other means. + * * This is only used for the list view. * + * @readonly + * @alpha + */ + hoveredFeatures: readonly(gfiListStoreRefs.hoveredFeatures), + + /** + * Selected feature. + * This is only used if the feature is loaded from the list view. + * * @alpha */ - hover: gfiListStore.hover, + selectedFeature: gfiMainStoreRefs.olFeature, /** * Selected features. * This is only used if the feature is loaded from the list view. * + * @readonly * @alpha */ - selectedFeatures: gfiMainStoreRefs.selectedFeatures, + selectedFeatures: readonly(gfiMainStoreRefs.olFeatures), /** * Feature information for the currently selected feature(s). * * @alpha */ - featureInformation: gfiMainStoreRefs.featureInformation, + featureInformation: gfiMainStoreRefs.geojsonFeatures, /** * List of features that should be displayed in a list view. @@ -177,12 +192,9 @@ export const useGfiStore = defineStore('plugins/gfi', () => { * Selected feature for the detail view. * This is the currently shown feature. * - * To mutate this value, change `featureIndex`. - * - * @readonly * @alpha */ - feature: gfiFeatureStoreRefs.selectedFeature, + feature: gfiMainStoreRefs.geojsonFeature, /** * Properties for the selected feature in detail view. diff --git a/src/plugins/gfi/stores/feature.ts b/src/plugins/gfi/stores/feature.ts index 56be4518e4..ef48a45b6d 100644 --- a/src/plugins/gfi/stores/feature.ts +++ b/src/plugins/gfi/stores/feature.ts @@ -5,7 +5,7 @@ import type { GfiLayerConfiguration, RequestGfiParameters } from '../types' import { rawLayerList } from '@masterportal/masterportalapi' import { debounce, isEqual } from 'es-toolkit' import { acceptHMRUpdate, defineStore } from 'pinia' -import { computed, nextTick, onScopeDispose, ref, watch } from 'vue' +import { computed, nextTick, onScopeDispose, watch } from 'vue' import { useRefStore } from '@/composables/useRefStore' import { useStoreWatcher } from '@/composables/useStoreWatcher' @@ -85,10 +85,10 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { if (options.toggleSelection) { Object.entries(result).forEach(([layerId, features]) => { - if (!gfiMainStore.featureInformation[layerId]) { - gfiMainStore.featureInformation[layerId] = [] + if (!gfiMainStore.geojsonFeatures[layerId]) { + gfiMainStore.geojsonFeatures[layerId] = [] } - const layerFeatureList = gfiMainStore.featureInformation[layerId] + const layerFeatureList = gfiMainStore.geojsonFeatures[layerId] features.forEach((feature) => { const oldFeatureIndex = layerFeatureList.findIndex((oldFeature) => @@ -104,7 +104,7 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { return } - gfiMainStore.featureInformation = result + gfiMainStore.geojsonFeatures = result } const waitMs = computed(() => gfiMainStore.configuration.waitMs || 50) @@ -118,7 +118,7 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { coordinate as RequestGfiParameters['coordinateOrExtent'] ) } else { - gfiMainStore.featureInformation = {} + gfiMainStore.geojsonFeatures = {} } }, { immediate: true } @@ -136,7 +136,7 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { gfiMainStore.configuration.multiSelect?.toggleSelection ?? true, }) } else { - gfiMainStore.featureInformation = {} + gfiMainStore.geojsonFeatures = {} } }) } @@ -161,20 +161,30 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { } const visibleFeatures = computed(() => - Object.entries(gfiMainStore.featureInformation) + Object.entries(gfiMainStore.geojsonFeatures) .filter(([layerId]) => gfiMainStore.configuration.layers[layerId]?.window) .flatMap(([layerId, features]) => features.map((feature) => ({ layerId, feature })) ) ) - const selectedFeatureIndex = ref(0) - const selectedFeature = computed( - () => visibleFeatures.value[selectedFeatureIndex.value] - ) + const selectedFeatureIndex = computed({ + get: () => + visibleFeatures.value.findIndex((feature) => + isEqual(feature, gfiMainStore.geojsonFeature) + ), + set: (value) => { + const feature = visibleFeatures.value[value] + if (feature) { + gfiMainStore.geojsonFeature = feature + } + }, + }) const selectedFeatureLayerConfiguration = computed(() => - gfiMainStore.getLayerConfiguration(selectedFeature.value?.layerId || '') + gfiMainStore.getLayerConfiguration( + gfiMainStore.geojsonFeature?.layerId || '' + ) ) const exportPropertyLayerConfiguration = computed( @@ -183,7 +193,7 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { const exportProperty = computed(() => exportPropertyLayerConfiguration.value - ? selectedFeature.value?.feature.properties?.[ + ? gfiMainStore.geojsonFeature?.feature.properties?.[ exportPropertyLayerConfiguration.value ] : null @@ -196,8 +206,8 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { ) const title = computed(() => - titleLayerConfiguration.value && selectedFeature.value - ? titleLayerConfiguration.value(selectedFeature.value.feature) + titleLayerConfiguration.value && gfiMainStore.geojsonFeature + ? titleLayerConfiguration.value(gfiMainStore.geojsonFeature.feature) : null ) @@ -207,7 +217,7 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { const selectedFeatureProperties = computed(() => Object.fromEntries( - Object.entries(selectedFeature.value?.feature.properties || {}) + Object.entries(gfiMainStore.geojsonFeature?.feature.properties || {}) .filter( ([key]) => (!selectedFeaturePropertiesLayerConfiguration.value || @@ -222,15 +232,10 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { ) watch( - visibleFeatures, - () => { - selectedFeatureIndex.value = 0 - }, - { deep: true } - ) - - watch( - [() => gfiMainStore.configuration.coordinateTarget, selectedFeature], + [ + () => gfiMainStore.configuration.coordinateTarget, + () => gfiMainStore.geojsonFeature, + ], ([target, feature], [, oldFeature]) => { if (target && !feature && oldFeature) { const targetStore = useRefStore(target) @@ -257,11 +262,14 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { void nextTick(() => { const iconMenuStore = coreStore.getPluginStore('iconMenu') if (iconMenuStore) { - watch(selectedFeature, (newFeature) => { - if (newFeature) { - iconMenuStore.openMenuById(PluginId) + watch( + () => gfiMainStore.geojsonFeature, + (newFeature) => { + if (newFeature) { + iconMenuStore.openMenuById(PluginId) + } } - }) + ) } }) } @@ -269,7 +277,6 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => { return { visibleFeatures, selectedFeatureIndex, - selectedFeature, selectedFeatureProperties, exportProperty, title, diff --git a/src/plugins/gfi/stores/list.ts b/src/plugins/gfi/stores/list.ts index 8779bc6a1a..236d0643a2 100644 --- a/src/plugins/gfi/stores/list.ts +++ b/src/plugins/gfi/stores/list.ts @@ -2,7 +2,7 @@ import type { Feature } from 'ol' import type { FeatureList } from '../types' import { acceptHMRUpdate, defineStore, storeToRefs } from 'pinia' -import { computed, markRaw, ref, shallowRef } from 'vue' +import { computed, markRaw, ref, shallowRef, watch } from 'vue' import { useOlVectorSources } from '@/composables/useOlVectorSources' import { useRefStore } from '@/composables/useRefStore' @@ -24,32 +24,11 @@ export const useGfiListStore = defineStore('plugins/gfi/list', () => { const configuration = computed(() => gfiMainStore.configuration.featureList) + const hoveredFeature = shallowRef<{ + layerId: string + feature: Feature + } | null>(null) const hoveredFeatures = shallowRef>({}) - if (configuration.value?.bindWithCoreHoverSelect) { - useBindWithCoreHoverSelect( - hoveredFeatures, - gfiMainStoreRefs.selectedFeatures, - coreStoreRefs.hoveredFeature, - coreStoreRefs.selectedFeature - ) - } - - function hover(data: { layerId: string; feature: Feature } | null) { - if (data === null) { - if (Object.keys(hoveredFeatures.value).length) { - hoveredFeatures.value = markRaw({}) - } - return - } - - const { layerId, feature } = data - if (hoveredFeatures.value[layerId]?.includes(feature)) { - return - } - hoveredFeatures.value = markRaw({ - [layerId]: markRaw([feature]), - }) - } const activeLayers = computed((): string[] => { if (!configuration.value) { @@ -170,8 +149,12 @@ export const useGfiListStore = defineStore('plugins/gfi/list', () => { paginatedFeatures.value.map((feature) => ({ ...feature, get hovered() { - return Object.values(hoveredFeatures.value).some((features) => - features.includes(feature.feature) + return ( + Object.values(hoveredFeatures.value).some((features) => + features.includes(feature.feature) + ) || + (configuration.value?.bindWithCoreHoverSelect && + coreStore.hoveredClusterFeatures.includes(feature.feature)) ) }, text: { @@ -182,10 +165,46 @@ export const useGfiListStore = defineStore('plugins/gfi/list', () => { })) ) + if (configuration.value?.bindWithCoreHoverSelect) { + useBindWithCoreHoverSelect( + // hovered feature + hoveredFeature, + coreStoreRefs.hoveredFeature, + // hovered cluster + hoveredFeatures, + coreStoreRefs.hoveredClusterFeatures, + // selected feature + gfiMainStoreRefs.olFeature, + coreStoreRefs.selectedFeature, + // selected cluster + gfiMainStoreRefs.olFeatures, + coreStoreRefs.selectedClusterFeatures, + // reference order + computed(() => flatFeatures.value.map(({ feature }) => feature)) + ) + } else { + watch(hoveredFeature, (value) => { + if (value === null) { + hoveredFeatures.value = {} + return + } + const { layerId, feature } = value + hoveredFeatures.value = { [layerId]: [feature] } + }) + watch(gfiMainStoreRefs.olFeature, (value) => { + if (value === null) { + gfiMainStore.olFeatures = {} + return + } + const { layerId, feature } = value + gfiMainStore.olFeatures = { [layerId]: [feature] } + }) + } + return { features, + hoveredFeature, hoveredFeatures, - hover, flatFeatures, paginationActive, pageLength, diff --git a/src/plugins/gfi/stores/main.ts b/src/plugins/gfi/stores/main.ts index 19b8093e68..70c030875a 100644 --- a/src/plugins/gfi/stores/main.ts +++ b/src/plugins/gfi/stores/main.ts @@ -35,7 +35,8 @@ export const useGfiMainStore = defineStore('plugins/gfi/main', () => { return configuration.value.layers[layerId] } - const { selectedFeatures, featureInformation } = useSelectedFeatures() + const { olFeatures, olFeature, geojsonFeatures, geojsonFeature } = + useSelectedFeatures() const customHighlightStyle = computed( () => @@ -51,7 +52,7 @@ export const useGfiMainStore = defineStore('plugins/gfi/main', () => { }) ) const highlightedFeatures = computed(() => - Object.entries(featureInformation.value) + Object.entries(geojsonFeatures.value) .filter(([layerId]) => getLayerConfiguration(layerId)?.geometry ?? true) .flatMap(([, features]) => features) ) @@ -65,8 +66,10 @@ export const useGfiMainStore = defineStore('plugins/gfi/main', () => { configuration, renderType, getLayerConfiguration, - selectedFeatures, - featureInformation, + olFeatures, + olFeature, + geojsonFeatures, + geojsonFeature, } })