From 2f99431f2d1809864120efddb71a12595569a7f2 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Tue, 13 Jan 2026 17:20:29 +0100
Subject: [PATCH 01/47] refactor(reverseGeocoder): introduce shared lib
getRefStore
---
src/lib/getRefStore.ts | 13 +++++++++++++
src/plugins/reverseGeocoder/store.ts | 9 +++------
2 files changed, 16 insertions(+), 6 deletions(-)
create mode 100644 src/lib/getRefStore.ts
diff --git a/src/lib/getRefStore.ts b/src/lib/getRefStore.ts
new file mode 100644
index 0000000000..bd466c7396
--- /dev/null
+++ b/src/lib/getRefStore.ts
@@ -0,0 +1,13 @@
+import type { StoreReference } from '@/core'
+import { useCoreStore } from '@/core/stores/export'
+
+/**
+ * Get the store for a `StoreReference`.
+ *
+ * @param ref - Store reference
+ * @returns Referenced store
+ */
+export function getRefStore(ref: StoreReference) {
+ const coreStore = useCoreStore()
+ return ref.plugin ? coreStore.getPluginStore(ref.plugin) : coreStore
+}
diff --git a/src/plugins/reverseGeocoder/store.ts b/src/plugins/reverseGeocoder/store.ts
index d39f2f70d9..07e2070a2c 100644
--- a/src/plugins/reverseGeocoder/store.ts
+++ b/src/plugins/reverseGeocoder/store.ts
@@ -12,6 +12,7 @@ import { acceptHMRUpdate, defineStore } from 'pinia'
import { computed, ref, watch, type Reactive, type WatchHandle } from 'vue'
import { useCoreStore } from '@/core/stores'
+import { getRefStore } from '@/lib/getRefStore'
import { indicateLoading } from '@/lib/indicateLoading'
import {
@@ -41,9 +42,7 @@ export const useReverseGeocoderStore = defineStore(
function setupPlugin() {
for (const source of configuration.value.coordinateSources || []) {
- const store = source.plugin
- ? coreStore.getPluginStore(source.plugin)
- : coreStore
+ const store = getRefStore(source)
if (!store) {
continue
}
@@ -71,9 +70,7 @@ export const useReverseGeocoderStore = defineStore(
target: NonNullable,
feature: ReverseGeocoderFeature
) {
- const targetStore = target.plugin
- ? coreStore.getPluginStore(target.plugin)
- : coreStore
+ const targetStore = getRefStore(target)
if (!targetStore) {
return
}
From 74e98dd1cde43759cd05805b3849ddca85e5f1b3 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Tue, 13 Jan 2026 17:22:14 +0100
Subject: [PATCH 02/47] feat(core): export `center` and `extent` as instable
API
---
src/core/components/PolarMap.ce.vue | 4 +++-
src/core/stores/index.ts | 15 +++++++++++++++
src/core/stores/main.ts | 3 +++
3 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/src/core/components/PolarMap.ce.vue b/src/core/components/PolarMap.ce.vue
index 7f788a0422..dad3ac922b 100644
--- a/src/core/components/PolarMap.ce.vue
+++ b/src/core/components/PolarMap.ce.vue
@@ -31,13 +31,15 @@ import { updateDragAndZoomInteractions } from '../utils/map/updateDragAndZoomInt
import PolarMapOverlay from './PolarMapOverlay.ce.vue'
const mainStore = useMainStore()
-const { hasWindowSize, hasSmallDisplay, center, zoom } = storeToRefs(mainStore)
+const { hasWindowSize, hasSmallDisplay, center, extent, zoom } =
+ storeToRefs(mainStore)
const polarMapContainer = useTemplateRef('polar-map-container')
const overlay = useTemplateRef('polar-map-overlay')
function onMove() {
center.value = mainStore.map.getView().getCenter() || center.value
+ extent.value = mainStore.map.getView().calculateExtent()
zoom.value = mainStore.map.getView().getZoom() || zoom.value
}
diff --git a/src/core/stores/index.ts b/src/core/stores/index.ts
index 88a82d49c4..24b92ed3ab 100644
--- a/src/core/stores/index.ts
+++ b/src/core/stores/index.ts
@@ -29,6 +29,13 @@ export const useCoreStore = defineStore('core', () => {
const markerStore = useMarkerStore()
return {
+ /**
+ * Read or modify center coordinate of the map.
+ *
+ * @internal
+ */
+ center: mainStoreRefs.center,
+
/**
* Color scheme the client should be using.
*
@@ -60,6 +67,14 @@ export const useCoreStore = defineStore('core', () => {
*/
deviceIsHorizontal: computed(() => mainStore.deviceIsHorizontal),
+ /**
+ * Extent of the map.
+ *
+ * @alpha
+ * @readonly
+ */
+ extent: computed(() => mainStore.extent),
+
/**
* Whether the map has a maximum height of {@link SMALL_DISPLAY_HEIGHT} and
* a maximum width of {@link SMALL_DISPLAY_WIDTH}.
diff --git a/src/core/stores/main.ts b/src/core/stores/main.ts
index e997721b57..de7e876255 100644
--- a/src/core/stores/main.ts
+++ b/src/core/stores/main.ts
@@ -1,5 +1,6 @@
import type { Feature, Map } from 'ol'
import type { Coordinate } from 'ol/coordinate'
+import type { Extent } from 'ol/extent'
import type { Point } from 'ol/geom'
import { toMerged } from 'es-toolkit'
@@ -27,6 +28,7 @@ export const useMainStore = defineStore('main', () => {
defaults
)
)
+ const extent = ref([0, 0, 0, 0])
const language = ref('')
const lightElement = ref(null)
const map = shallowRef({} as Map)
@@ -100,6 +102,7 @@ export const useMainStore = defineStore('main', () => {
serviceRegister,
shadowRoot,
center,
+ extent,
zoom,
// Getters
layout,
From 0da4c0aace7a1fc2e4cbbf30975169fa28fcecb7 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Fri, 16 Jan 2026 13:54:17 +0100
Subject: [PATCH 03/47] feat(core): add shared pagination component
---
src/components/kern/KernPagination.ce.vue | 154 ++++++++++++++++++++++
src/core/types/locales.ts | 12 +-
src/core/vuePlugins/i18next.ts | 7 +
src/locales.ts | 59 +++++++++
4 files changed, 229 insertions(+), 3 deletions(-)
create mode 100644 src/components/kern/KernPagination.ce.vue
create mode 100644 src/locales.ts
diff --git a/src/components/kern/KernPagination.ce.vue b/src/components/kern/KernPagination.ce.vue
new file mode 100644
index 0000000000..8d717fa4c3
--- /dev/null
+++ b/src/components/kern/KernPagination.ce.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
diff --git a/src/core/types/locales.ts b/src/core/types/locales.ts
index 43ac237249..496c9d0c06 100644
--- a/src/core/types/locales.ts
+++ b/src/core/types/locales.ts
@@ -2,7 +2,8 @@ import type { ResourceKey } from 'i18next'
import type { BundledPluginId, BundledPluginLocaleResources } from '@/core'
import type { resourcesEn as core } from '@/core/locales'
-import type { CoreId } from '@/core/vuePlugins/i18next'
+import type { resourcesEn as shared } from '@/locales'
+import type { CoreId, SharedId } from '@/core/vuePlugins/i18next'
/** @internal */
export interface Locale {
@@ -12,9 +13,14 @@ export interface Locale {
/** @internal */
export type LocaleResources = {
- [T in typeof CoreId | BundledPluginId]: T extends BundledPluginId
+ [T in
+ | typeof CoreId
+ | typeof SharedId
+ | BundledPluginId]: T extends BundledPluginId
? BundledPluginLocaleResources
- : typeof core
+ : T extends typeof SharedId
+ ? typeof shared
+ : typeof core
}
type ToLocaleOverride = T extends string
diff --git a/src/core/vuePlugins/i18next.ts b/src/core/vuePlugins/i18next.ts
index 17fee380a5..5c3291f0c3 100644
--- a/src/core/vuePlugins/i18next.ts
+++ b/src/core/vuePlugins/i18next.ts
@@ -5,8 +5,10 @@ import LanguageDetector from 'i18next-browser-languagedetector'
import I18NextVue from 'i18next-vue'
import locales from '../locales'
+import sharedLocales from '@/locales'
export const CoreId = 'core'
+export const SharedId = 'shared'
export const I18Next: Plugin = {
async install(app) {
@@ -28,6 +30,11 @@ export const I18Next: Plugin = {
supportedLngs: locales.map(({ type }) => type),
})
+ // This is no plugin itself, but bundled for usage of all plugins
+ sharedLocales.forEach((lng) => {
+ i18next.addResourceBundle(lng.type, SharedId, lng.resources, true)
+ })
+
// eslint-disable-next-line no-console
console.info(`Successfully initialized i18next.`)
} catch (error: unknown) {
diff --git a/src/locales.ts b/src/locales.ts
new file mode 100644
index 0000000000..28806ca6e1
--- /dev/null
+++ b/src/locales.ts
@@ -0,0 +1,59 @@
+/* eslint-disable tsdoc/syntax */
+/**
+ * This is the documentation for the locales keys for POLAR shared components.
+ * These locales are *NOT* exported, but documented only.
+ *
+ * @module locales/shared
+ */
+/* eslint-enable tsdoc/syntax */
+
+import type { Locale } from '@/core/types'
+
+/**
+ * German locales for POLAR core.
+ * For overwriting these values, pass a partial object of this in `locales`.
+ */
+export const resourcesDe = {
+ pagination: {
+ currentPage: 'Aktuelle Seite, Seite {{page}}',
+ page: 'Seite {{page}}',
+ next: 'Nächste Seite',
+ previous: 'Vorherige Seite',
+ wrapper: 'Seitenauswahl',
+ },
+} as const
+
+/**
+ * English locales for POLAR core.
+ * For overwriting these values, pass a partial object of this in `locales`.
+ */
+export const resourcesEn = {
+ pagination: {
+ currentPage: 'Aktuelle Seite, Seite {{page}}',
+ page: 'Seite {{page}}',
+ next: 'Nächste Seite',
+ previous: 'Vorherige Seite',
+ wrapper: 'Seitenauswahl',
+ },
+} as const
+
+/**
+ * Core locales.
+ *
+ * @privateRemarks
+ * The first entry will be used as fallback.
+ *
+ * @internal
+ */
+const locales: Locale[] = [
+ {
+ type: 'de',
+ resources: resourcesDe,
+ },
+ {
+ type: 'en',
+ resources: resourcesEn,
+ },
+]
+
+export default locales
From 15972bfff219673bb5a3ba109cbaae995271fcf1 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Tue, 13 Jan 2026 17:24:02 +0100
Subject: [PATCH 04/47] test(snowbox): allow jumping to Hamburg via button
---
examples/snowbox/YetAnotherEmptyComponent.vue | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/examples/snowbox/YetAnotherEmptyComponent.vue b/examples/snowbox/YetAnotherEmptyComponent.vue
index affcb15908..ca48bebf73 100644
--- a/examples/snowbox/YetAnotherEmptyComponent.vue
+++ b/examples/snowbox/YetAnotherEmptyComponent.vue
@@ -1,7 +1,17 @@
- Awesome
+
+ Awesome
+
+
From d449c8d5d3a0eb16984bb7c1c3b2490bdba8683c Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Mon, 16 Feb 2026 14:09:32 +0100
Subject: [PATCH 05/47] test(snowbox): debug assistant for browser devtools
The usually helpful variables (map, stores, etc.) are auto-exported as
global variables for use with the browser's devtools in snowbox.
---
examples/snowbox/debug-assistant.js | 25 +++++++++++++++++++++++++
examples/snowbox/index.html | 1 +
src/core/stores/index.ts | 16 ++++++++++++++++
src/core/stores/plugin.ts | 5 ++++-
4 files changed, 46 insertions(+), 1 deletion(-)
create mode 100644 examples/snowbox/debug-assistant.js
diff --git a/examples/snowbox/debug-assistant.js b/examples/snowbox/debug-assistant.js
new file mode 100644
index 0000000000..49a8b521ff
--- /dev/null
+++ b/examples/snowbox/debug-assistant.js
@@ -0,0 +1,25 @@
+import { getStore } from '@polar/polar'
+
+function initializeDebugAssistant() {
+ const map = document.getElementById('snowbox')
+ const coreStore = getStore(map, 'core')
+ const activePluginIds = coreStore.activePluginIds
+
+ window.map = map
+ window.olMap = coreStore.map
+ window.coreStore = coreStore
+ window.activePluginIds = activePluginIds
+ for (const pluginId of activePluginIds) {
+ window[`${pluginId}Store`] = coreStore.getPluginStore(pluginId)
+ }
+}
+
+// We want to load as late as possible.
+// Especially, the timeout stuff from snowbox code should be done when doing this.
+setTimeout(() => {
+ initializeDebugAssistant()
+
+ // 7 seconds may be long sometimes, inform the developer about it.
+ // eslint-disable-next-line no-console
+ console.info('POLAR debug assistant was successfully initialized')
+}, 7000)
diff --git a/examples/snowbox/index.html b/examples/snowbox/index.html
index 79a0317df1..69c45bfbe9 100644
--- a/examples/snowbox/index.html
+++ b/examples/snowbox/index.html
@@ -8,6 +8,7 @@
+
diff --git a/src/plugins/gfi/components/GfiFeatureList.ce.vue b/src/plugins/gfi/components/GfiFeatureList.ce.vue
new file mode 100644
index 0000000000..ba1291e255
--- /dev/null
+++ b/src/plugins/gfi/components/GfiFeatureList.ce.vue
@@ -0,0 +1,68 @@
+
+
+ {{ $t(($) => $.list.header, { ns: 'gfi' }) }}
+
+
+ {{ $t(($) => $.list.emptyView, { ns: 'gfi' }) }}
+
+
+
+ {
+ gfiStore.selectedFeatures = { [layerId]: [feature] }
+ gfiStore.hoveredFeatures = {}
+ })()
+ "
+ @mouseenter="gfiStore.hoveredFeatures = { [layerId]: [feature] }"
+ @mouseleave="gfiStore.hoveredFeatures = {}"
+ >
+
+ {{ gfiStore.listGetText(feature, 'title') }}
+
+ {{ gfiStore.listGetText(feature, 'subtitle') }}
+
+ {{ gfiStore.listGetText(feature, 'subSubtitle') }}
+
+
+
+
+
+
+
diff --git a/src/plugins/gfi/components/GfiUI.ce.vue b/src/plugins/gfi/components/GfiUI.ce.vue
new file mode 100644
index 0000000000..0a940eba09
--- /dev/null
+++ b/src/plugins/gfi/components/GfiUI.ce.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plugins/gfi/composables/useFeatureDisplayLayer.ts b/src/plugins/gfi/composables/useFeatureDisplayLayer.ts
new file mode 100644
index 0000000000..fb7e91a272
--- /dev/null
+++ b/src/plugins/gfi/composables/useFeatureDisplayLayer.ts
@@ -0,0 +1,86 @@
+import type { Feature as GeoJsonFeature } from 'geojson'
+import type { Style } from 'ol/style'
+
+import { Feature, Map } from 'ol'
+import { GeoJSON } from 'ol/format'
+import VectorLayer from 'ol/layer/Vector'
+import { Vector } from 'ol/source'
+import { onScopeDispose, watch, type Ref } from 'vue'
+
+function getFeatureDisplayLayer() {
+ const featureDisplayLayer = new VectorLayer({
+ source: new Vector({
+ features: [],
+ }),
+ })
+
+ featureDisplayLayer.set('polarInternalId', 'pluginGfiFeatureDisplay')
+ featureDisplayLayer.setZIndex(90)
+ // NOTE: This may be changed in the future to not use the default styling of @masterportal/masterportalapi
+ featureDisplayLayer.set('styleId', 'defaultHighlightFeaturesPoint')
+
+ return featureDisplayLayer
+}
+
+function isVectorSource(source): source is Vector {
+ return source instanceof Vector
+}
+
+/**
+ * reset feature layer's features.
+ */
+function clear(featureDisplayLayer: VectorLayer): void {
+ const source = featureDisplayLayer.getSource()
+ if (isVectorSource(source)) {
+ source.clear()
+ }
+}
+
+/**
+ * add feature from jsonable GeoJson object.
+ */
+function addFeature(
+ feature: GeoJsonFeature,
+ featureDisplayLayer: VectorLayer
+): void {
+ const source = featureDisplayLayer.getSource()
+ if (isVectorSource(source)) {
+ // Since ol@10, readFeature may also return a Feature[]?
+ source.addFeature(new GeoJSON().readFeature(feature) as Feature)
+ }
+}
+
+export function useFeatureDisplayLayer(options: {
+ map: Map
+ style: Ref
diff --git a/src/plugins/gfi/stores/feature.ts b/src/plugins/gfi/stores/feature.ts
index ac0d63d13c..8369151744 100644
--- a/src/plugins/gfi/stores/feature.ts
+++ b/src/plugins/gfi/stores/feature.ts
@@ -4,13 +4,16 @@ import { rawLayerList } from '@masterportal/masterportalapi'
import { isEqual } from 'es-toolkit'
import { MapBrowserEvent, Overlay } from 'ol'
import { acceptHMRUpdate, defineStore } from 'pinia'
-import { computed, onScopeDispose, ref, watch } from 'vue'
+import { computed, nextTick, onScopeDispose, ref, watch } from 'vue'
import { useCoreStore } from '@/core/stores'
-import type { GfiLayerConfiguration, RequestGfiParameters } from '../types'
-
import { useMultiSelection } from '../composables/useMultiSelection'
+import {
+ PluginId,
+ type GfiLayerConfiguration,
+ type RequestGfiParameters,
+} from '../types'
import { requestGfi } from '../utils/requestGfi'
import { updateTooltip } from '../utils/updateTooltip'
import { useGfiMainStore } from './main'
@@ -236,6 +239,21 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => {
coreStore.map.removeOverlay(overlay)
})
+ if (gfiMainStore.configuration.renderType === 'iconMenu') {
+ // TODO: Find a better solution to wait for this plugin
+ // As, in this case, we render as part of the iconMenu, the iconMenu store will be available soon.
+ void nextTick(() => {
+ const iconMenuStore = coreStore.getPluginStore('iconMenu')
+ if (iconMenuStore) {
+ watch(selectedFeature, (newFeature) => {
+ if (newFeature) {
+ iconMenuStore.openMenuById(PluginId)
+ }
+ })
+ }
+ })
+ }
+
return {
visibleFeatures,
selectedFeatureIndex,
From 978d7183564ba9dd6a5f888092cb6c0b9838ac28 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings <142312676+oeninghe-dataport@users.noreply.github.com>
Date: Mon, 2 Mar 2026 10:24:55 +0100
Subject: [PATCH 09/47] fix(gfi): remove legacy manual plugin prefix for
console
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Pascal Röhling <73653210+dopenguin@users.noreply.github.com>
---
src/plugins/gfi/utils/requestGfiWms.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/plugins/gfi/utils/requestGfiWms.ts b/src/plugins/gfi/utils/requestGfiWms.ts
index 24aaf48704..cba4bd2fdf 100644
--- a/src/plugins/gfi/utils/requestGfiWms.ts
+++ b/src/plugins/gfi/utils/requestGfiWms.ts
@@ -60,7 +60,7 @@ function readTextFeatures(text: string): Feature[] {
feature.set(key || '', value)
} else {
console.error(
- '@polar/plugin-gfi: Found property before feature declaration in readTextFeatures.',
+ 'Found property before feature declaration in readTextFeatures.',
line,
'Skipping ...'
)
From 6d6008610b652bce6a8af3ea91851e9567733d77 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings <142312676+oeninghe-dataport@users.noreply.github.com>
Date: Mon, 2 Mar 2026 10:25:56 +0100
Subject: [PATCH 10/47] docs: fix shared locales hint
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Pascal Röhling <73653210+dopenguin@users.noreply.github.com>
---
src/locales.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/locales.ts b/src/locales.ts
index 28806ca6e1..62c02c9aa6 100644
--- a/src/locales.ts
+++ b/src/locales.ts
@@ -10,7 +10,7 @@
import type { Locale } from '@/core/types'
/**
- * German locales for POLAR core.
+ * German locales for POLAR shared components.
* For overwriting these values, pass a partial object of this in `locales`.
*/
export const resourcesDe = {
From 6182677d1048092aafd7730eb1589250826de8a3 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings <142312676+oeninghe-dataport@users.noreply.github.com>
Date: Mon, 2 Mar 2026 10:26:08 +0100
Subject: [PATCH 11/47] docs: fix shared locales hint
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Pascal Röhling <73653210+dopenguin@users.noreply.github.com>
---
src/locales.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/locales.ts b/src/locales.ts
index 62c02c9aa6..1e89b2e026 100644
--- a/src/locales.ts
+++ b/src/locales.ts
@@ -38,7 +38,7 @@ export const resourcesEn = {
} as const
/**
- * Core locales.
+ * POLAR shared components locales.
*
* @privateRemarks
* The first entry will be used as fallback.
From 439ae45a5d2e8c26b5173dba24e72a9a0852ff47 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings <142312676+oeninghe-dataport@users.noreply.github.com>
Date: Mon, 2 Mar 2026 10:26:22 +0100
Subject: [PATCH 12/47] docs: fix shared locales hint
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Pascal Röhling <73653210+dopenguin@users.noreply.github.com>
---
src/locales.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/locales.ts b/src/locales.ts
index 1e89b2e026..071900ed54 100644
--- a/src/locales.ts
+++ b/src/locales.ts
@@ -24,7 +24,7 @@ export const resourcesDe = {
} as const
/**
- * English locales for POLAR core.
+ * English locales for POLAR shared components.
* For overwriting these values, pass a partial object of this in `locales`.
*/
export const resourcesEn = {
From 63a5d72d1600116f523581777689924d8b7f72b6 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Wed, 15 Apr 2026 20:24:39 +0200
Subject: [PATCH 13/47] docs(gfi): remove duplicate ToDo hint
---
src/plugins/gfi/utils/requestGfiWfs.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/plugins/gfi/utils/requestGfiWfs.ts b/src/plugins/gfi/utils/requestGfiWfs.ts
index 020d35545a..018c37ec89 100644
--- a/src/plugins/gfi/utils/requestGfiWfs.ts
+++ b/src/plugins/gfi/utils/requestGfiWfs.ts
@@ -57,7 +57,6 @@ export default ({
const { geometryName } = layerConfiguration
const code = map.getView().getProjection().getCode()
const typeName = version === '2.0.0' ? 'typeNames' : 'typename'
- // TODO: Layer list needs better typing
const featureUrl = new URL(url as string)
featureUrl.searchParams.set('service', 'WFS')
From 55ff02a5c82ac6277921ac862cd9e2b84ff78685 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Wed, 15 Apr 2026 20:31:56 +0200
Subject: [PATCH 14/47] docs(gfi): use typedoc-linking to layout nineRegions
---
src/core/types/plugin.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/core/types/plugin.ts b/src/core/types/plugin.ts
index 92144c5f34..df4613230f 100644
--- a/src/core/types/plugin.ts
+++ b/src/core/types/plugin.ts
@@ -55,7 +55,8 @@ export interface PluginOptions {
/**
* The region where the plugin should be rendered.
- * Required for nine-regions layout, ignored otherwise.
+ *
+ * Only usable if {@link MapConfiguration.layout | `layout`} is set to `'nineRegions'`.
*/
layoutTag?: keyof typeof NineLayoutTag
From c2edb5291cd4747f72447857ba24c886c3b03955 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Wed, 15 Apr 2026 21:14:47 +0200
Subject: [PATCH 15/47] feat(core): introduce InterfacePluginOptions
---
src/core/types/plugin.ts | 2 ++
src/plugins/attributions/types.ts | 4 ++--
src/plugins/fullscreen/types.ts | 4 ++--
src/plugins/geoLocation/types.ts | 5 +++--
src/plugins/gfi/types.ts | 4 ++--
src/plugins/layerChooser/index.ts | 8 ++++++--
6 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/core/types/plugin.ts b/src/core/types/plugin.ts
index df4613230f..66f67fab48 100644
--- a/src/core/types/plugin.ts
+++ b/src/core/types/plugin.ts
@@ -59,7 +59,9 @@ export interface PluginOptions {
* Only usable if {@link MapConfiguration.layout | `layout`} is set to `'nineRegions'`.
*/
layoutTag?: keyof typeof NineLayoutTag
+}
+export interface InterfacePluginOptions extends PluginOptions {
/**
* Defines if the plugin is rendered independent or as part of the icon menu.
* This is automatically set by the icon menu; you should not need to touch this.
diff --git a/src/plugins/attributions/types.ts b/src/plugins/attributions/types.ts
index 714f13296d..00cdaeccb9 100644
--- a/src/plugins/attributions/types.ts
+++ b/src/plugins/attributions/types.ts
@@ -1,4 +1,4 @@
-import type { PluginOptions, StoreReference } from '@/core'
+import type { InterfacePluginOptions, StoreReference } from '@/core'
export const PluginId = 'attributions'
@@ -63,7 +63,7 @@ export interface Attribution {
* All parameters are optional. However, setting neither {@link layerAttributions}
* nor {@link staticAttributions} results in an empty window.
*/
-export interface AttributionsPluginOptions extends PluginOptions {
+export interface AttributionsPluginOptions extends InterfacePluginOptions {
/**
* Optional icon override.
*/
diff --git a/src/plugins/fullscreen/types.ts b/src/plugins/fullscreen/types.ts
index 1e12f43c35..9d9a7515b3 100644
--- a/src/plugins/fullscreen/types.ts
+++ b/src/plugins/fullscreen/types.ts
@@ -1,4 +1,4 @@
-import type { PluginOptions } from '@/core'
+import type { InterfacePluginOptions } from '@/core'
/**
* Plugin identifier.
@@ -8,7 +8,7 @@ export const PluginId = 'fullscreen'
/**
* Plugin options for fullscreen plugin.
*/
-export interface FullscreenPluginOptions extends PluginOptions {
+export interface FullscreenPluginOptions extends InterfacePluginOptions {
/**
* Defines the target container to show in fullscreen mode.
* This defaults to the web component (i.e., the map with its plugin controls).
diff --git a/src/plugins/geoLocation/types.ts b/src/plugins/geoLocation/types.ts
index 6e96297d32..b656dc8f4d 100644
--- a/src/plugins/geoLocation/types.ts
+++ b/src/plugins/geoLocation/types.ts
@@ -1,4 +1,4 @@
-import type { LayerBoundPluginOptions } from '@/core'
+import type { LayerBoundPluginOptions, InterfacePluginOptions } from '@/core'
/**
* Plugin identifier.
@@ -13,7 +13,8 @@ export type PluginState = 'LOCATABLE' | 'LOCATED' | 'DISABLED'
/**
* Plugin options for geoLocation plugin.
*/
-export interface GeoLocationPluginOptions extends LayerBoundPluginOptions {
+export interface GeoLocationPluginOptions
+ extends LayerBoundPluginOptions, InterfacePluginOptions {
/**
* If `true`, the location check will be run on map start-up. If `false`, the
* feature has to be triggered with a button press by the user.
diff --git a/src/plugins/gfi/types.ts b/src/plugins/gfi/types.ts
index 22bd13c344..3e8b36bf93 100644
--- a/src/plugins/gfi/types.ts
+++ b/src/plugins/gfi/types.ts
@@ -7,7 +7,7 @@ import type { ImageWMS, TileWMS } from 'ol/source'
import type { Options as Fill } from 'ol/style/Fill'
import type { Options as Stroke } from 'ol/style/Stroke'
-import type { PluginOptions, StoreReference } from '@/core'
+import type { InterfacePluginOptions, StoreReference } from '@/core'
/**
* Plugin identifier.
@@ -238,7 +238,7 @@ export interface FeatureList {
* }
* ```
*/
-export interface GfiPluginOptions extends PluginOptions {
+export interface GfiPluginOptions extends InterfacePluginOptions {
/**
* Maps a string (must be a layer ID) to a behaviour configuration for that layer.
*/
diff --git a/src/plugins/layerChooser/index.ts b/src/plugins/layerChooser/index.ts
index 3e406f2f19..c0a6d5516f 100644
--- a/src/plugins/layerChooser/index.ts
+++ b/src/plugins/layerChooser/index.ts
@@ -4,7 +4,11 @@
*/
/* eslint-enable tsdoc/syntax */
-import type { PluginContainer, PluginOptions, PolarPluginStore } from '@/core'
+import type {
+ PluginContainer,
+ InterfacePluginOptions,
+ PolarPluginStore,
+} from '@/core'
import component from './components/LayerChooser.ce.vue'
import locales from './locales'
@@ -25,7 +29,7 @@ import { PluginId } from './types'
* @returns Plugin for use with {@link addPlugin}.
*/
export default function pluginLayerChooser(
- options: PluginOptions
+ options: InterfacePluginOptions
): PluginContainer {
return {
id: PluginId,
From 79852f313b59b0e25e2344c51bd520cc5dc8c579 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Wed, 15 Apr 2026 21:47:27 +0200
Subject: [PATCH 16/47] refactor(gfi): introduce new composable
usePluginStoreWatcher
---
src/plugins/gfi/stores/feature.ts | 33 +++++++++++++------------------
1 file changed, 14 insertions(+), 19 deletions(-)
diff --git a/src/plugins/gfi/stores/feature.ts b/src/plugins/gfi/stores/feature.ts
index 8369151744..91cf640fc8 100644
--- a/src/plugins/gfi/stores/feature.ts
+++ b/src/plugins/gfi/stores/feature.ts
@@ -6,6 +6,7 @@ import { MapBrowserEvent, Overlay } from 'ol'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { computed, nextTick, onScopeDispose, ref, watch } from 'vue'
+import { usePluginStoreWatcher } from '@/composables/usePluginStoreWatcher'
import { useCoreStore } from '@/core/stores'
import { useMultiSelection } from '../composables/useMultiSelection'
@@ -108,25 +109,19 @@ export const useGfiFeatureStore = defineStore('plugins/gfi/feature', () => {
gfiMainStore.featureInformation = result
}
- for (const source of gfiMainStore.configuration.coordinateSources || []) {
- const store = source.plugin
- ? coreStore.getPluginStore(source.plugin)
- : coreStore
- if (!store) {
- continue
- }
- watch(
- () => store[source.key],
- async (coordinate) => {
- if (coordinate) {
- await getFeatureInfo(coordinate)
- } else {
- gfiMainStore.featureInformation = {}
- }
- },
- { immediate: true }
- )
- }
+ usePluginStoreWatcher(
+ gfiMainStore.configuration.coordinateSources || [],
+ async (coordinate) => {
+ if (coordinate) {
+ await getFeatureInfo(
+ coordinate as RequestGfiParameters['coordinateOrExtent']
+ )
+ } else {
+ gfiMainStore.featureInformation = {}
+ }
+ },
+ { immediate: true }
+ )
if (gfiMainStore.configuration.multiSelect) {
const multiSelection = useMultiSelection({
From 9f2541387f0486a0b408601530631914017a0c95 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Thu, 16 Apr 2026 16:05:12 +0200
Subject: [PATCH 17/47] fix(core): consider hoveredFeature set from plugin
again
---
src/core/utils/map/setupMarkers.ts | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/src/core/utils/map/setupMarkers.ts b/src/core/utils/map/setupMarkers.ts
index 89281abb7e..51c3e7e380 100644
--- a/src/core/utils/map/setupMarkers.ts
+++ b/src/core/utils/map/setupMarkers.ts
@@ -222,13 +222,19 @@ export function setupMarkers(map: Map) {
}
if (feature !== null && feature !== toRaw(store.selected)) {
store.hovered = markRaw(feature)
- const isMultiFeature = store.hovered.get('features')?.length > 1
- const style = getMarkerStyle(
- getLayerConfiguration(feature.get('_polarLayerId') as string)
- .hoverStyle,
- isMultiFeature
+ const layerId = feature.get('_polarLayerId') as string
+ const selectedCluster =
+ // @ts-expect-error | Found layers always have a source and getDistance is defined on cluster sources.
+ typeof findLayer(map, layerId)?.getSource().getDistance === 'function'
+ ? getCluster(map, feature, '_polarLayerId')
+ : feature
+ selectedCluster.setStyle(
+ getMarkerStyle(
+ getLayerConfiguration(feature.get('_polarLayerId') as string)
+ .hoverStyle,
+ selectedCluster.get('features')?.length > 1
+ )
)
- feature.setStyle(style)
}
}
)
From d15b2c2353723ec832a3e7d74f3eb042686e8bb2 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Thu, 16 Apr 2026 16:09:07 +0200
Subject: [PATCH 18/47] chore(core): markers: fix double star in comment
---
src/core/utils/markers.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core/utils/markers.ts b/src/core/utils/markers.ts
index 33e82e45e6..a14e2d3b4c 100644
--- a/src/core/utils/markers.ts
+++ b/src/core/utils/markers.ts
@@ -71,7 +71,7 @@ const anchor = [0.5, 1]
/**
* The map became a little laggy due to constant re-generation of styles.
* This memoization function optimises this issue by reusing styles.
- * */
+ */
const memoizeStyle = (getMarker: GetMarkerFunction): GetMarkerFunction => {
const singleCache = new Map()
const multiCache = new Map()
From 4dfe6afafec4d3d4e80347114b8619c61a0e02d7 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Thu, 16 Apr 2026 16:09:41 +0200
Subject: [PATCH 19/47] test(snowbox): enable bindWithCoreHoverSelect for gfi
plugin
---
examples/snowbox/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/snowbox/index.js b/examples/snowbox/index.js
index e5172c1fac..6eebb74ca4 100644
--- a/examples/snowbox/index.js
+++ b/examples/snowbox/index.js
@@ -447,7 +447,7 @@ addPlugin(
key: 'activeMaskIds',
},
mode: 'visible',
- bindWithCoreHoverSelect: false,
+ bindWithCoreHoverSelect: true,
pageLength: 5,
text: {
title: (feature) =>
From 875a35cb25a0e88c47fec7b97b66c80482d20052 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Thu, 16 Apr 2026 16:45:21 +0200
Subject: [PATCH 20/47] fix(gfi): wrap text for empty view in feature list
---
src/plugins/gfi/components/GfiFeatureList.ce.vue | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/plugins/gfi/components/GfiFeatureList.ce.vue b/src/plugins/gfi/components/GfiFeatureList.ce.vue
index ba1291e255..f34b453b8b 100644
--- a/src/plugins/gfi/components/GfiFeatureList.ce.vue
+++ b/src/plugins/gfi/components/GfiFeatureList.ce.vue
@@ -2,9 +2,12 @@
{{ $t(($) => $.list.header, { ns: 'gfi' }) }}
-
+
{{ $t(($) => $.list.emptyView, { ns: 'gfi' }) }}
-
+
diff --git a/vue2/packages/plugins/Gfi/src/components/FeatureButtonGroup.vue b/vue2/packages/plugins/Gfi/src/components/FeatureButtonGroup.vue
deleted file mode 100644
index d40a83342f..0000000000
--- a/vue2/packages/plugins/Gfi/src/components/FeatureButtonGroup.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
diff --git a/vue2/packages/plugins/Gfi/src/components/FeatureSwitchButtons.vue b/vue2/packages/plugins/Gfi/src/components/FeatureSwitchButtons.vue
deleted file mode 100644
index 4afd420dd9..0000000000
--- a/vue2/packages/plugins/Gfi/src/components/FeatureSwitchButtons.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
- fa-chevron-left
-
-
- fa-chevron-right
-
-
-
-
-
-
-
diff --git a/vue2/packages/plugins/Gfi/src/components/FeatureTableBody.vue b/vue2/packages/plugins/Gfi/src/components/FeatureTableBody.vue
deleted file mode 100644
index 242f38c226..0000000000
--- a/vue2/packages/plugins/Gfi/src/components/FeatureTableBody.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
- | {{ key }} |
-
-
-
-
- |
-
-
- {{ 'Link' }}
-
- |
-
- {{ value }}
- |
-
-
-
-
-
-
-
diff --git a/vue2/packages/plugins/Gfi/src/components/FeatureTableHead.vue b/vue2/packages/plugins/Gfi/src/components/FeatureTableHead.vue
deleted file mode 100644
index 890b2f3d39..0000000000
--- a/vue2/packages/plugins/Gfi/src/components/FeatureTableHead.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- | {{ $t('plugins.gfi.header.field') }} |
- {{ $t('plugins.gfi.header.value') }} |
-
-
-
-
-
-
-
diff --git a/vue2/packages/plugins/Gfi/src/components/Gfi.vue b/vue2/packages/plugins/Gfi/src/components/Gfi.vue
deleted file mode 100644
index f81b8c34c3..0000000000
--- a/vue2/packages/plugins/Gfi/src/components/Gfi.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
-
- {{ $t('plugins.gfi.noActiveLayer') }}
-
-
-
-
-
-
-
-
diff --git a/vue2/packages/plugins/Gfi/src/components/List.vue b/vue2/packages/plugins/Gfi/src/components/List.vue
deleted file mode 100644
index 32ecd7c103..0000000000
--- a/vue2/packages/plugins/Gfi/src/components/List.vue
+++ /dev/null
@@ -1,212 +0,0 @@
-
-
-
-
- fa-list
- {{ $t('plugins.gfi.list.header') }}
-
-
- {{
- `${$t('plugins.gfi.list.entry')} ${page * pageLength + 1} ${$t(
- 'plugins.gfi.list.to'
- )} ${Math.min((page + 1) * pageLength, listFeatures.length)} ${$t(
- 'plugins.gfi.list.of'
- )} ${listFeatures.length}`
- }}
-
-
-
-
- {{ $t('plugins.gfi.list.emptyView') }}
-
-
-
-
- {{ $t(applyListText(feature, index)) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vue2/packages/plugins/Gfi/src/components/index.ts b/vue2/packages/plugins/Gfi/src/components/index.ts
deleted file mode 100644
index 317c7e8a7c..0000000000
--- a/vue2/packages/plugins/Gfi/src/components/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Gfi } from './Gfi.vue'
diff --git a/vue2/packages/plugins/Gfi/src/store/actions/index.ts b/vue2/packages/plugins/Gfi/src/store/actions/index.ts
deleted file mode 100644
index 58d3996d60..0000000000
--- a/vue2/packages/plugins/Gfi/src/store/actions/index.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-import debounce from 'lodash.debounce'
-import { Feature as GeoJsonFeature } from 'geojson'
-import { Fill, Stroke, Style } from 'ol/style'
-import { GeoJSON } from 'ol/format'
-import { Feature } from 'ol'
-import { PolarActionTree } from '@polar/lib-custom-types'
-import { filterFeatures } from '../../utils/filterFeatures'
-import { renderFeatures } from '../../utils/renderFeatures'
-import { clear, getFeatureDisplayLayer } from '../../utils/displayFeatureLayer'
-import { FeaturesByLayerId, GfiGetters, GfiState } from '../../types'
-import { debouncedGfiRequest } from './debouncedGfiRequest'
-import { setupCoreListener, setupTooltip, setupZoomListeners } from './setup'
-import { setupMultiSelection } from './setupMultiSelection'
-
-export const makeActions = () => {
- const writer = new GeoJSON()
- const featureDisplayLayer = getFeatureDisplayLayer()
-
- let debouncedVisibilityChangeIndicator
-
- const actions: PolarActionTree = {
- /**
- * Responsible for setting up the module by
- * - adding watchers to configured coordinate sources. Whenever one of these
- * sources changes, the module is reset and all information is fetched
- * anew.
- * - adding the geometry display layer to the map.
- */
- setupModule({
- getters: { gfiConfiguration, defaultHighlightStyle },
- rootGetters,
- dispatch,
- }): void {
- const { coordinateSources, customHighlightStyle } = gfiConfiguration
- const { map } = rootGetters
-
- const reaction = (coordinate) => {
- clear(featureDisplayLayer)
- if (coordinate && coordinate.length) {
- dispatch('getFeatureInfo', { coordinateOrExtent: coordinate })
- }
- }
-
- coordinateSources.forEach((coordinateSource) =>
- this.watch(() => rootGetters[coordinateSource], reaction)
- )
-
- map.addLayer(featureDisplayLayer)
- featureDisplayLayer.setStyle(
- new Style({
- stroke: new Stroke(
- customHighlightStyle?.stroke || defaultHighlightStyle.stroke
- ),
- fill: new Fill(
- customHighlightStyle?.fill || defaultHighlightStyle.fill
- ),
- })
- )
-
- dispatch('setupTooltip')
- dispatch('setupFeatureVisibilityUpdates')
- dispatch('setupCoreListener')
- dispatch('setupZoomListeners')
- dispatch('setupMultiSelection')
- },
- setupCoreListener,
- setupMultiSelection,
- setupTooltip,
- setupZoomListeners,
- setupFeatureVisibilityUpdates({ commit, state, getters, rootGetters }) {
- // debounce to prevent update spam
- debouncedVisibilityChangeIndicator = debounce(
- () =>
- commit(
- 'setVisibilityChangeIndicator',
- state.visibilityChangeIndicator + 1
- ),
- 10
- )
- rootGetters.map
- .getLayers()
- .getArray()
- .forEach((layer) => {
- if (getters.layerKeys.includes(layer.get('id'))) {
- layer
- // @ts-expect-error | layers reaching this have a source
- .getSource()
- .on('addfeature', debouncedVisibilityChangeIndicator)
- }
- })
- },
- close({ commit, dispatch, rootGetters }, userInteraction = false) {
- commit('clearFeatureInformation')
- commit('setImageLoaded', false)
- // NOTE: null is needed, as the payload is always the second argument...
- if (
- !rootGetters.configuration?.extendedMasterportalapiMarkers &&
- userInteraction
- ) {
- dispatch('plugin/pins/removeMarker', null, { root: true })
- }
- dispatch('setCoreSelection', { feature: null })
- clear(featureDisplayLayer) // ... features of gfi layer
- },
- /**
- * Action getFeatureInfo
- * 1. resets the module state
- * 2. fetches new feature information for each configured layer
- * 3. adds features to the display layer optionally (if configured)
- */
- async getFeatureInfo(
- { commit, dispatch },
- coordinateOrExtent: [number, number] | [number, number, number, number]
- ): Promise {
- 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', coordinateOrExtent)
- },
- debouncedGfiRequest: debouncedGfiRequest(featureDisplayLayer),
- setCoreSelection(
- { commit, dispatch, rootGetters },
- {
- feature,
- centerOnFeature = false,
- }: { feature: Feature | null; centerOnFeature?: boolean }
- ) {
- if (rootGetters.selected !== feature) {
- commit('setSelected', feature, { root: true })
- dispatch(
- 'updateSelection',
- { feature, centerOnFeature },
- { root: true }
- )
- }
- },
- setOlFeatureInformation(
- { commit, dispatch },
- {
- feature,
- centerOnFeature = false,
- }: { feature: Feature | null; centerOnFeature?: boolean }
- ) {
- commit('clearFeatureInformation')
- commit('setVisibleWindowFeatureIndex', 0)
- clear(featureDisplayLayer)
- if (feature !== null) {
- commit('setFeatureInformation', {
- [feature.get('_gfiLayerId')]: feature.get('features')?.length
- ? feature
- .get('features')
- .map((feature) => JSON.parse(writer.writeFeature(feature)))
- : [JSON.parse(writer.writeFeature(feature))],
- })
- 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 })
- }
- },
- unhover({ commit, rootGetters }) {
- if (rootGetters.configuration.extendedMasterportalapiMarkers) {
- commit('setHovered', null, { root: true })
- }
- },
- }
-
- return actions
-}
diff --git a/vue2/packages/plugins/Gfi/src/store/actions/setup.ts b/vue2/packages/plugins/Gfi/src/store/actions/setup.ts
index e4450e51c1..6a20a5043d 100644
--- a/vue2/packages/plugins/Gfi/src/store/actions/setup.ts
+++ b/vue2/packages/plugins/Gfi/src/store/actions/setup.ts
@@ -1,79 +1,9 @@
import { PolarActionContext, PolarStore } from '@polar/lib-custom-types'
import { GeoJsonProperties } from 'geojson'
import getCluster from '@polar/lib-get-cluster'
-import { getTooltip, Tooltip } from '@polar/lib-tooltip'
-import Overlay from 'ol/Overlay'
-import { Feature } from 'ol'
import { GfiGetters, GfiState } from '../../types'
import { getOriginalFeature } from '../../utils/getOriginalFeature'
-export function setupCoreListener(
- this: PolarStore,
- {
- getters: { gfiConfiguration },
- rootGetters,
- dispatch,
- }: PolarActionContext
-) {
- if (gfiConfiguration.featureList?.bindWithCoreHoverSelect) {
- this.watch(
- () => rootGetters.selected,
- (feature) => dispatch('setOlFeatureInformation', { feature }),
- { deep: true }
- )
- }
-}
-
-export function setupTooltip({
- getters: { gfiConfiguration },
- rootGetters: { map },
-}: PolarActionContext) {
- 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)
- }
- })
-}
-
export function setupZoomListeners(
this: PolarStore,
{ dispatch, getters, rootGetters }: PolarActionContext
diff --git a/vue2/packages/plugins/Gfi/src/store/actions/setupMultiSelection.ts b/vue2/packages/plugins/Gfi/src/store/actions/setupMultiSelection.ts
deleted file mode 100644
index 72e5e0467e..0000000000
--- a/vue2/packages/plugins/Gfi/src/store/actions/setupMultiSelection.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { Map } from 'ol'
-import { Modify } from 'ol/interaction'
-import Draw, {
- createBox,
- type Options as DrawOptions,
-} from 'ol/interaction/Draw'
-import { platformModifierKeyOnly } from 'ol/events/condition'
-import { Fill, Stroke, Style } from 'ol/style'
-import { PolarActionContext } from '@polar/lib-custom-types'
-import { GfiGetters, GfiState } from '../../types'
-
-const isDrawing = (map: Map) =>
- map
- .getInteractions()
- .getArray()
- .some(
- (interaction) =>
- (interaction instanceof Draw &&
- // @ts-expect-error | internal hack to detect it from @polar/plugin-gfi and @polar/plugin-draw
- (interaction._isMultiSelect || interaction._isDrawPlugin)) ||
- interaction instanceof Modify ||
- // @ts-expect-error | internal hack to detect it from @polar/plugin-draw
- interaction._isDeleteSelect ||
- // @ts-expect-error | internal hack to detect it from @polar/plugin-measure
- interaction._isMeasureSelect
- )
-
-const drawOptions: DrawOptions = {
- stopClick: true,
- type: 'Circle',
- style: new Style({
- stroke: new Stroke({ color: 'white', width: 1.5 }),
- fill: new Fill({ color: [255, 255, 255, 0.75] }),
- }),
- freehandCondition: (event) => {
- if (event.type === 'pointermove') {
- return false
- } else if (event.type === 'pointerup') {
- return true
- }
- return platformModifierKeyOnly(event)
- },
- condition: () => false,
-}
-
-export function setupMultiSelection({
- dispatch,
- getters: {
- gfiConfiguration: { boxSelect, directSelect, multiSelect },
- },
- rootGetters,
-}: PolarActionContext) {
- if (boxSelect || multiSelect === 'box' || multiSelect === 'circle') {
- if (boxSelect) {
- console.warn(
- '@polar/plugin-gfi: Configuration parameter "boxSelect" has been deprecated. Please use the new parameter "multiSelect" set to "box" instead.'
- )
- }
- if (multiSelect !== 'circle') {
- drawOptions.geometryFunction = createBox()
- } else {
- delete drawOptions.geometryFunction
- }
- const draw = new Draw(drawOptions)
- draw.on('drawstart', () => {
- // @ts-expect-error | internal hack to detect it in @polar/plugin-pins
- draw._isMultiSelect = true
- })
- draw.on('drawabort', () => {
- // @ts-expect-error | internal hack to detect it in @polar/plugin-pins
- draw._isMultiSelect = false
- })
- draw.on('drawend', (e) =>
- dispatch('getFeatureInfo', {
- // @ts-expect-error | A feature that is drawn has a geometry.
- coordinateOrExtent: e.feature.getGeometry().getExtent(),
- modifierPressed: true,
- }).finally(() => {
- // @ts-expect-error | internal hack to detect it in @polar/plugin-pins
- draw._isMultiSelect = false
- })
- )
- rootGetters.map.addInteraction(draw)
- }
- if (directSelect) {
- rootGetters.map.on('click', ({ coordinate, originalEvent }) => {
- if (!isDrawing(rootGetters.map)) {
- dispatch('getFeatureInfo', {
- coordinateOrExtent: coordinate,
- modifierPressed:
- navigator.userAgent.indexOf('Mac') !== -1
- ? originalEvent.metaKey
- : originalEvent.ctrlKey,
- })
- }
- })
- }
-}
diff --git a/vue2/packages/plugins/Gfi/src/store/getInitialState.ts b/vue2/packages/plugins/Gfi/src/store/getInitialState.ts
deleted file mode 100644
index 4604a6b610..0000000000
--- a/vue2/packages/plugins/Gfi/src/store/getInitialState.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { GfiState } from '../types'
-
-const getInitialState = (): GfiState => ({
- featureInformation: {},
- imageLoaded: false,
- visibleWindowFeatureIndex: 0,
- visibilityChangeIndicator: 0,
- defaultHighlightStyle: {
- stroke: {
- color: '#003064',
- width: 3,
- },
- fill: {
- color: 'rgb(255, 255, 255, 0.7)',
- },
- },
- page: 0,
-})
-
-export default getInitialState
diff --git a/vue2/packages/plugins/Gfi/src/store/getters.ts b/vue2/packages/plugins/Gfi/src/store/getters.ts
deleted file mode 100644
index 36b2b01c6b..0000000000
--- a/vue2/packages/plugins/Gfi/src/store/getters.ts
+++ /dev/null
@@ -1,254 +0,0 @@
-import { generateSimpleGetters } from '@repositoryname/vuex-generators'
-import { GfiConfiguration, PolarGetterTree } from '@polar/lib-custom-types'
-import noop from '@repositoryname/noop'
-import { isVisible } from '@polar/lib-invisible-style'
-import { Cluster as ClusterSource } from 'ol/source'
-import { GeoJSON } from 'ol/format'
-import { GfiGetters, GfiState } from '../types'
-import { listableLayersFilter } from '../utils/listableLayersFilter'
-import getInitialState from './getInitialState'
-
-const getters: PolarGetterTree = {
- ...generateSimpleGetters(getInitialState()),
- gfiConfiguration(_, __, ___, rootGetters) {
- return (rootGetters.configuration?.gfi || {
- afterLoadFunction: null,
- coordinateSources: [],
- layers: {},
- })
- },
- renderType(_, __, ___, rootGetters) {
- return rootGetters.configuration?.gfi?.renderType || 'independent'
- },
- renderMoveHandle(_, getters, __, rootGetters) {
- return (
- getters.renderType === 'independent' &&
- rootGetters.hasWindowSize &&
- rootGetters.hasSmallWidth
- )
- },
- gfiContentComponent(_, { gfiConfiguration }) {
- return gfiConfiguration.gfiContentComponent || null
- },
- afterLoadFunction(_, { gfiConfiguration }) {
- return typeof gfiConfiguration?.afterLoadFunction === 'function'
- ? gfiConfiguration.afterLoadFunction
- : null
- },
- currentProperties(
- _,
- { exportPropertyLayerKeys, visibleWindowFeatureIndex, windowFeatures }
- ) {
- const properties = {
- ...windowFeatures[visibleWindowFeatureIndex],
- }
- const exportProperty =
- exportPropertyLayerKeys[properties.polarInternalLayerKey]
- if (exportProperty?.length > 0) {
- delete properties[exportProperty]
- }
- return properties
- },
- layerKeys(_, { gfiConfiguration }) {
- return Object.keys(gfiConfiguration.layers)
- },
- exportProperty(
- _,
- {
- currentProperties,
- exportPropertyLayerKeys,
- visibleWindowFeatureIndex,
- windowFeatures,
- }
- ) {
- if (currentProperties) {
- const property =
- exportPropertyLayerKeys[currentProperties.polarInternalLayerKey]
- return property.length > 0
- ? (windowFeatures[visibleWindowFeatureIndex]?.[property] as string)
- : ''
- }
- return ''
- },
- exportPropertyLayerKeys(_, { gfiConfiguration }) {
- return Object.entries(gfiConfiguration.layers).reduce(
- (accumulator, [key, { exportProperty }]) => ({
- ...accumulator,
- [key]: typeof exportProperty === 'string' ? exportProperty : '',
- }),
- {} as Record
- )
- },
- /** only show switch buttons if multiple property sets are available */
- showSwitchButtons(_, { windowFeatures }) {
- return windowFeatures.length > 1
- },
- windowLayerKeys(_, { gfiConfiguration }) {
- return Object.entries(gfiConfiguration.layers).reduce(
- (accumulator, [key, { window }]) => {
- if (window) {
- return [...accumulator, key]
- }
- return accumulator
- },
- [] as string[]
- )
- },
- windowLayerKeysActive(
- _,
- { windowLayerKeys, gfiConfiguration },
- __,
- rootGetters
- ) {
- const { activeLayerPath } = gfiConfiguration
- if (!activeLayerPath) {
- // if not configured, restriction does not apply
- return true
- }
- // update on change indicator
- noop(rootGetters[activeLayerPath])
- return Boolean(
- rootGetters.map
- .getLayers()
- .getArray()
- .filter(
- (layer) =>
- windowLayerKeys.includes(layer.get('id')) && layer.getVisible()
- ).length
- )
- },
- geometryLayerKeys(_, { gfiConfiguration }) {
- return Object.entries(gfiConfiguration.layers).reduce(
- (accumulator, [key, { geometry }]) => {
- if (geometry) {
- return [...accumulator, key]
- }
- return accumulator
- },
- [] as string[]
- )
- },
- windowFeatures(_, { featureInformation, windowLayerKeys, gfiConfiguration }) {
- return Object.entries(featureInformation)
- .map(([key, features]) =>
- /*
- NOTE: When displaying the features in the map, the first feature is rendered first
- thus lying under every other following feature. However, when using the gfi window,
- the first feature is initially displayed in the window.
- To have both in line, the feature displayed on top, the order of the features is reversed here for the window.
- */
- windowLayerKeys.includes(key) && Array.isArray(features)
- ? features.reverse().map(({ properties }) => {
- const baseProperties = { polarInternalLayerKey: key }
- const propertyReducer = gfiConfiguration.layers[key].properties
-
- // if it is an Array, just forward properties named in it
- if (Array.isArray(propertyReducer)) {
- return {
- ...Object.fromEntries(
- Object.entries(properties || {}).filter(([key]) =>
- propertyReducer.includes(key)
- )
- ),
- ...baseProperties,
- }
- }
- // if of type object, map keys, forward properties named in object keys
- if (typeof propertyReducer === 'object') {
- const relevantKeys = Object.keys(propertyReducer)
- return {
- ...Object.fromEntries(
- Object.entries(properties || {})
- .filter(([key]) => relevantKeys.includes(key))
- .map(([key, value]) => [propertyReducer[key], value])
- ),
- ...baseProperties,
- }
- }
-
- // if neither, just forward properties
- return { ...properties, ...baseProperties }
- })
- : []
- )
- .flat(1)
- },
- listMode(_, { gfiConfiguration }) {
- if (gfiConfiguration.featureList && !gfiConfiguration.featureList.mode) {
- console.error(
- '@polar/plugin-gfi: When using featureList a mode has to be chosen.'
- )
- }
- return gfiConfiguration.featureList?.mode
- },
- listText(_, { gfiConfiguration }) {
- return gfiConfiguration.featureList?.text || []
- },
- showList(_, { windowFeatures, gfiConfiguration }) {
- return Boolean(gfiConfiguration.featureList && !windowFeatures.length)
- },
- listableLayerSources(_, { layerKeys }, __, rootGetters) {
- return rootGetters.map
- .getLayers()
- .getArray()
- .filter((layer) => layerKeys.includes(layer.get('id')))
- .filter(listableLayersFilter)
- .map((layer) => {
- // @ts-expect-error | no sourceless layers in masterportalAPI generation
- let source = layer.getSource()
- while (source instanceof ClusterSource) {
- source = source.getSource()
- }
- source.set('_gfiLayerId', layer.get('id'), true)
- return source
- })
- },
- listFeatures(
- { visibilityChangeIndicator },
- { gfiConfiguration, listableLayerSources, listMode },
- __,
- rootGetters
- ) {
- const { map, clientHeight, clientWidth, center, zoomLevel } = rootGetters
- const writer = new GeoJSON()
- // trigger getter on those who indicate feature change possibility
- noop(clientHeight, clientWidth, center, zoomLevel)
- noop(visibilityChangeIndicator)
- return listableLayerSources
- .map((source) => {
- const layerId = source.get('_gfiLayerId')
- return (
- listMode === 'loaded'
- ? source.getFeatures()
- : source.getFeaturesInExtent(
- map.getView().calculateExtent(map.getSize()),
- map.getView().getProjection()
- )
- )
- .filter(isVisible)
- .filter((feature) => {
- const { isSelectable } = gfiConfiguration.layers[layerId]
- return typeof isSelectable === 'function'
- ? isSelectable(JSON.parse(writer.writeFeature(feature)))
- : true
- })
- .map((feature) => {
- // true = silent change (prevents cluster recomputation & rerender)
- feature.set('_gfiLayerId', layerId, true)
- return feature
- })
- })
- .flat(1)
- },
- isFeatureHovered: (_, __, ___, rootGetters) => (feature) => {
- const { hovered } = rootGetters
- return (
- hovered !== null &&
- (hovered === feature || hovered.get('features')
- ? hovered.get('features').includes(feature)
- : false)
- )
- },
-}
-
-export default getters
diff --git a/vue2/packages/plugins/Gfi/src/utils/filterFeatures.ts b/vue2/packages/plugins/Gfi/src/utils/filterFeatures.ts
deleted file mode 100644
index 3e63bb1d37..0000000000
--- a/vue2/packages/plugins/Gfi/src/utils/filterFeatures.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Feature as GeoJsonFeature } from 'geojson'
-import { FeaturesByLayerId } from '../types'
-
-export function filterFeatures(
- featuresByLayerId: FeaturesByLayerId
-): Record {
- const entries = Object.entries(featuresByLayerId)
- const filtered = entries.filter((keyValue) => Array.isArray(keyValue[1])) as [
- string,
- GeoJsonFeature[]
- ][]
- return Object.fromEntries(filtered)
-}
diff --git a/vue2/packages/plugins/Gfi/tests/actions.spec.ts b/vue2/packages/plugins/Gfi/tests/actions.spec.ts
deleted file mode 100644
index 26d87a44ec..0000000000
--- a/vue2/packages/plugins/Gfi/tests/actions.spec.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-// NOTE: action tests currently not type-supported, but working
-/* eslint-disable @typescript-eslint/ban-ts-comment */
-import { makeActions } from '../src/store/actions'
-
-jest.mock('../src/utils/requestGfi', () => ({
- requestGfi: jest.fn(() => Promise.resolve([])),
-}))
-
-jest.mock('../src/utils/displayFeatureLayer', () => ({
- getFeatureDisplayLayer: jest.fn(),
- addFeature: jest.fn(),
- clear: jest.fn(),
-}))
-
-describe('GFI Actions', () => {
- const actions = makeActions()
- beforeEach(() => {
- jest.clearAllMocks()
- jest.useFakeTimers()
- })
- it('should call getFeatureInfo with the correct parameters', async () => {
- const dispatch = jest.fn()
- const coordinate = [0, 0]
-
- const rootGetters = {
- map: {
- getLayers: () => ({
- getArray: () => [],
- }),
- getView: () => ({
- getProjection: () => ({
- getCode: () => 'EPSG:4326',
- }),
- }),
- },
- }
- const configuration = {
- gfi: {
- layers: {},
- mode: 'bboxDot',
- maxFeatures: 10,
- },
- }
- const getters = {
- layerKeys: ['layer1'],
- geometryLayerKeys: [],
- afterLoadFunction: null,
- gfiConfiguration: configuration.gfi,
- }
-
- const context = {
- commit: jest.fn(),
- dispatch,
- rootGetters,
- configuration,
- getters,
- }
-
- const getFeatureInfo = actions.getFeatureInfo
- // @ts-ignore
- await getFeatureInfo(context, coordinate)
-
- expect(dispatch).toHaveBeenCalledTimes(1)
- expect(dispatch).toHaveBeenCalledWith('debouncedGfiRequest', coordinate)
- })
- it('should fetch and process features', async () => {
- const commit = jest.fn()
- const configuration = {
- gfi: {
- layers: {
- layer1: {
- maxFeatures: 10,
- },
- },
- mode: 'bboxDot',
- maxFeatures: 10,
- },
- }
- const rootGetters = {
- map: {
- getLayers: () => ({
- getArray: () => [],
- }),
- getView: () => ({
- getProjection: () => ({
- getCode: () => 'EPSG:4326',
- }),
- }),
- },
- configuration,
- }
- const getters = {
- layerKeys: ['layer1'],
- geometryLayerKeys: ['layer1'],
- afterLoadFunction: null,
- gfiConfiguration: configuration.gfi,
- }
-
- const context = {
- commit,
- rootGetters,
- getters,
- }
-
- const coordinate = [0, 0]
- const layerResponse = []
-
- const allSettledResult = [{ status: 'fulfilled', value: layerResponse }]
- const allSettledMock = jest.spyOn(Promise, 'allSettled')
- // @ts-ignore
- allSettledMock.mockResolvedValue(allSettledResult)
-
- const debounceMock = jest.fn((fn) => fn)
- jest.mock('lodash', () => ({
- debounce: debounceMock,
- }))
-
- const debouncedFunction = actions.debouncedGfiRequest
- // @ts-ignore
- const debouncedPromise = debouncedFunction(context, coordinate)
- // @ts-ignore
- jest.runAllTimers()
-
- await debouncedPromise
-
- expect(context.commit).toHaveBeenCalledTimes(1)
- expect(context.commit).toHaveBeenCalledWith('setFeatureInformation', {
- layer1: [],
- })
- })
- it('should handle failed feature request', async () => {
- const commit = jest.fn()
- const configuration = {
- gfi: {
- layers: {
- layer1: {
- maxFeatures: 10,
- },
- },
- mode: 'bboxDot',
- maxFeatures: 10,
- },
- }
- const rootGetters = {
- map: {
- getLayers: () => ({
- getArray: () => [],
- }),
- getView: () => ({
- getProjection: () => ({
- getCode: () => 'EPSG:4326',
- }),
- }),
- },
- configuration,
- }
- const getters = {
- layerKeys: ['layer1'],
- geometryLayerKeys: ['layer1'],
- afterLoadFunction: null,
- gfiConfiguration: configuration.gfi,
- }
-
- const context = {
- commit,
- rootGetters,
- getters,
- }
-
- const coordinate = [0, 0]
-
- const allSettledResult = [
- {
- status: 'rejected',
- reason: { message: 'Feature request failed' },
- },
- ]
- const allSettledMock = jest.spyOn(Promise, 'allSettled')
- // @ts-ignore
- allSettledMock.mockResolvedValue(allSettledResult)
-
- const debounceMock = jest.fn((fn) => fn)
- jest.mock('lodash', () => ({
- debounce: debounceMock,
- }))
-
- const debouncedFunction = actions.debouncedGfiRequest
- // @ts-ignore
- const debouncedPromise = debouncedFunction(context, coordinate)
- // @ts-ignore
- jest.runAllTimers()
-
- await debouncedPromise
-
- expect(context.commit).toHaveBeenCalledTimes(1)
- expect(context.commit).toHaveBeenCalledWith('setFeatureInformation', {
- layer1: expect.any(Symbol),
- })
- })
-})
diff --git a/vue2/packages/plugins/Gfi/tests/filterFeatures.spec.ts b/vue2/packages/plugins/Gfi/tests/filterFeatures.spec.ts
deleted file mode 100644
index 60cdfe5c85..0000000000
--- a/vue2/packages/plugins/Gfi/tests/filterFeatures.spec.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { filterFeatures } from '../src/utils/filterFeatures'
-
-describe('filterFeatures', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- jest.useFakeTimers()
- })
-
- it('should return an object with only GeoJsonFeature[] as its values if the values were already only GeoJsonFeature[]', () => {
- expect(
- Object.keys(
- filterFeatures({
- idOne: [
- {
- type: 'Feature',
- geometry: { type: 'Point', coordinates: [0, 0] },
- properties: { goodFeature: true },
- },
- ],
- idTwo: [
- {
- type: 'Feature',
- geometry: { type: 'Point', coordinates: [1, 1] },
- properties: { goodFeature: true },
- },
- {
- type: 'Feature',
- geometry: { type: 'Point', coordinates: [0, 1] },
- properties: { goodFeature: false },
- },
- ],
- })
- ).length
- ).toBe(2)
- })
- it('should return an object with less keys if some values are symbols', () => {
- expect(
- Object.keys(
- filterFeatures({
- idOne: Symbol('failing'),
- idTwo: [
- {
- type: 'Feature',
- geometry: { type: 'Point', coordinates: [1, 1] },
- properties: { goodFeature: true },
- },
- {
- type: 'Feature',
- geometry: { type: 'Point', coordinates: [0, 1] },
- properties: { goodFeature: false },
- },
- ],
- })
- ).length
- ).toBe(1)
- })
- it('should return an empty object if all values are symbols', () => {
- expect(
- Object.keys(
- filterFeatures({
- idOne: Symbol('failing'),
- idTwo: Symbol('another fail'),
- })
- ).length
- ).toBe(0)
- })
-})
diff --git a/vue2/packages/plugins/Gfi/tests/sortFeatures.spec.ts b/vue2/packages/plugins/Gfi/tests/sortFeatures.spec.ts
deleted file mode 100644
index fd9d7a9e42..0000000000
--- a/vue2/packages/plugins/Gfi/tests/sortFeatures.spec.ts
+++ /dev/null
@@ -1,235 +0,0 @@
-import { Feature as GeoJsonFeature, LineString, Point, Polygon } from 'geojson'
-import sortFeatures from '../src/utils/sortFeatures'
-
-describe('sortFeatures', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- jest.useFakeTimers()
- })
-
- describe('sorting two features', () => {
- it('should sort the features by area (biggest first) if both are Polygons', () => {
- const featureA: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Polygon',
- coordinates: [
- [
- [10.5, 59],
- [15, 59],
- [15, 56],
- [10, 56],
- [10.5, 59],
- ],
- ],
- },
- properties: {},
- }
- const featureB: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Polygon',
- coordinates: [
- [
- [-4.25, 62],
- [27.5, 62],
- [26.5, 45],
- [-4.25, 50],
- [-4.5, 62.25],
- ],
- ],
- },
- properties: {},
- }
- expect(
- [featureA, featureB].sort((a, b) => sortFeatures(a, b, 'EPSG:4326'))
- ).toEqual([featureB, featureA])
- })
- it('should put the Polygon feature first if the other feature is not a Polygon', () => {
- const featureA: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'LineString',
- coordinates: [
- [1.5, 55],
- [20, 55],
- ],
- },
- properties: {},
- }
- const featureB: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Polygon',
- coordinates: [
- [
- [10.5, 59],
- [15, 59],
- [15, 56],
- [10, 56],
- [10.5, 59],
- ],
- ],
- },
- properties: {},
- }
- expect(
- [featureA, featureB].sort((a, b) => sortFeatures(a, b, 'EPSG:4326'))
- ).toEqual([featureB, featureA])
- })
- it('should should keep the original order if both features are LineStrings', () => {
- const featureA: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'LineString',
- coordinates: [
- [1.5, 55],
- [20, 55],
- ],
- },
- properties: {},
- }
- const featureB: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'LineString',
- coordinates: [
- [1.5, 55],
- [20, 55],
- [10, 56],
- ],
- },
- properties: {},
- }
- expect(
- [featureA, featureB].sort((a, b) => sortFeatures(a, b, 'EPSG:4326'))
- ).toEqual([featureA, featureB])
- })
- it('should put the LineString feature first if the other feature is a Point Feature', () => {
- const featureA: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [1.5, 55],
- },
- properties: {},
- }
- const featureB: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'LineString',
- coordinates: [
- [1.5, 55],
- [20, 55],
- [10, 56],
- ],
- },
- properties: {},
- }
- expect(
- [featureA, featureB].sort((a, b) => sortFeatures(a, b, 'EPSG:4326'))
- ).toEqual([featureB, featureA])
- })
- it('should should keep the original order if both features are Points', () => {
- const featureA: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [1.5, 55],
- },
- properties: {},
- }
- const featureB: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [55, 1.5],
- },
- properties: {},
- }
- expect(
- [featureA, featureB].sort((a, b) => sortFeatures(a, b, 'EPSG:4326'))
- ).toEqual([featureA, featureB])
- })
- })
- describe('sorting more than two features', () => {
- it('should order the features by Polygons first, then LineStrings and lastly Points', () => {
- const featureA: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'LineString',
- coordinates: [
- [1.5, 55],
- [20, 55],
- ],
- },
- properties: {},
- }
- const featureB: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'LineString',
- coordinates: [
- [1.5, 55],
- [20, 55],
- [10, 56],
- ],
- },
- properties: {},
- }
- const featureC: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [1.5, 55],
- },
- properties: {},
- }
- const featureD: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Polygon',
- coordinates: [
- [
- [10.5, 59],
- [15, 59],
- [15, 56],
- [10, 56],
- [10.5, 59],
- ],
- ],
- },
- properties: {},
- }
- const featureE: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [55, 1.5],
- },
- properties: {},
- }
- const featureF: GeoJsonFeature = {
- type: 'Feature',
- geometry: {
- type: 'Polygon',
- coordinates: [
- [
- [-4.25, 62],
- [27.5, 62],
- [26.5, 45],
- [-4.25, 50],
- [-4.5, 62.25],
- ],
- ],
- },
- properties: {},
- }
- expect(
- [featureA, featureB, featureC, featureD, featureE, featureF].sort(
- (a, b) => sortFeatures(a, b, 'EPSG:4326')
- )
- ).toEqual([featureF, featureD, featureA, featureB, featureC, featureE])
- })
- })
-})
From cf9760741fc81b107b55390e1929eff98f75782c Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Wed, 13 May 2026 13:36:39 +0200
Subject: [PATCH 44/47] style: introduce ESLint rule
perfectionist/sort-named-imports
---
.husky/prepareConfig.js | 2 +-
eslint.config.ts | 1 +
examples/github-io/components/DevExSection.vue | 2 +-
examples/iceberg/stores/iceberg.ts | 2 +-
scripts/create-github-release.ts | 2 +-
scripts/typedoc-install-vue-tsc.ts | 2 +-
src/client.ts | 4 ++--
src/composables/usePluginStoreWatcher.ts | 2 +-
src/core/composables/useT.ts | 2 +-
src/core/stores/plugin.ts | 6 +++---
src/core/utils/map/setupMarkers.ts | 2 +-
src/plugins/addressSearch/store.ts | 2 +-
src/plugins/attributions/index.ts | 2 +-
src/plugins/filter/components/FilterUI.spec.ts | 2 +-
src/plugins/filter/index.ts | 2 +-
src/plugins/filter/stores/main.ts | 2 +-
src/plugins/footer/index.ts | 2 +-
.../fullscreen/components/FullscreenUI.spec.ts | 2 +-
src/plugins/fullscreen/index.ts | 2 +-
src/plugins/fullscreen/store.ts | 2 +-
.../geoLocation/components/GeoLocation.spec.ts | 2 +-
src/plugins/geoLocation/index.ts | 2 +-
src/plugins/geoLocation/store.ts | 2 +-
src/plugins/geoLocation/types.ts | 2 +-
src/plugins/gfi/components/GfiFeature.ce.vue | 11 +++++++++--
src/plugins/gfi/components/GfiFeatureList.ce.vue | 2 +-
src/plugins/gfi/composables/useFeatureDisplayLayer.ts | 2 +-
src/plugins/gfi/index.ts | 2 +-
src/plugins/gfi/stores/feature.ts | 2 +-
src/plugins/gfi/stores/main.ts | 2 +-
src/plugins/gfi/utils/updateTooltip.ts | 2 +-
src/plugins/iconMenu/index.ts | 2 +-
src/plugins/layerChooser/index.ts | 2 +-
src/plugins/pins/index.ts | 2 +-
src/plugins/pins/utils/getPinStyle.ts | 2 +-
src/plugins/scale/index.ts | 2 +-
src/plugins/scale/store.ts | 2 +-
src/plugins/toast/components/ToastUI.spec.ts | 2 +-
src/plugins/toast/store.ts | 2 +-
vite.config.ts | 2 +-
40 files changed, 51 insertions(+), 43 deletions(-)
diff --git a/.husky/prepareConfig.js b/.husky/prepareConfig.js
index 9e04d1517c..2736be2893 100644
--- a/.husky/prepareConfig.js
+++ b/.husky/prepareConfig.js
@@ -1,4 +1,4 @@
-import { readFileSync, existsSync } from 'node:fs'
+import { existsSync, readFileSync } from 'node:fs'
let config = JSON.parse(readFileSync('.husky/defaults.json').toString())
if (existsSync('git-hooks.config.json')) {
diff --git a/eslint.config.ts b/eslint.config.ts
index c80710ab3f..924ae09916 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -43,6 +43,7 @@ const polarConfig = defineConfig({
],
'import-x/order': 'off',
'perfectionist/sort-imports': 'error',
+ 'perfectionist/sort-named-imports': 'error',
'vue/html-self-closing': [
'error',
{
diff --git a/examples/github-io/components/DevExSection.vue b/examples/github-io/components/DevExSection.vue
index 61ef6af8a7..fe7bc6fdba 100644
--- a/examples/github-io/components/DevExSection.vue
+++ b/examples/github-io/components/DevExSection.vue
@@ -100,7 +100,7 @@
diff --git a/src/plugins/gfi/composables/useFeatureDisplayLayer.ts b/src/plugins/gfi/composables/useFeatureDisplayLayer.ts
index 90f5c07d23..eba44623f1 100644
--- a/src/plugins/gfi/composables/useFeatureDisplayLayer.ts
+++ b/src/plugins/gfi/composables/useFeatureDisplayLayer.ts
@@ -5,7 +5,7 @@ import { Feature, Map } from 'ol'
import { GeoJSON } from 'ol/format'
import VectorLayer from 'ol/layer/Vector'
import { Vector } from 'ol/source'
-import { onScopeDispose, watch, type Ref } from 'vue'
+import { onScopeDispose, type Ref, watch } from 'vue'
function getFeatureDisplayLayer() {
const featureDisplayLayer = new VectorLayer({
diff --git a/src/plugins/gfi/index.ts b/src/plugins/gfi/index.ts
index 65e47f5764..f630665e1e 100644
--- a/src/plugins/gfi/index.ts
+++ b/src/plugins/gfi/index.ts
@@ -9,7 +9,7 @@ import type { PluginContainer, PolarPluginStore } from '@/core'
import component from './components/GfiUI.ce.vue'
import locales from './locales'
import { useGfiStore } from './store'
-import { PluginId, type GfiPluginOptions } from './types'
+import { type GfiPluginOptions, PluginId } from './types'
/**
* Creates a plugin which fetches and optionally displays GFI (GetFeatureInfo) from WMS and WFS services or layers based on GeoJSON files.
diff --git a/src/plugins/gfi/stores/feature.ts b/src/plugins/gfi/stores/feature.ts
index 91cf640fc8..26f755cee9 100644
--- a/src/plugins/gfi/stores/feature.ts
+++ b/src/plugins/gfi/stores/feature.ts
@@ -11,8 +11,8 @@ import { useCoreStore } from '@/core/stores'
import { useMultiSelection } from '../composables/useMultiSelection'
import {
- PluginId,
type GfiLayerConfiguration,
+ PluginId,
type RequestGfiParameters,
} from '../types'
import { requestGfi } from '../utils/requestGfi'
diff --git a/src/plugins/gfi/stores/main.ts b/src/plugins/gfi/stores/main.ts
index 39c02c734b..5a161fd647 100644
--- a/src/plugins/gfi/stores/main.ts
+++ b/src/plugins/gfi/stores/main.ts
@@ -9,9 +9,9 @@ import { useCoreStore } from '@/core/stores'
import { useFeatureDisplayLayer } from '../composables/useFeatureDisplayLayer'
import {
- PluginId,
type CustomHighlightStyle,
type GfiPluginOptions,
+ PluginId,
} from '../types'
import { serializeFeature } from '../utils/serializeFeature'
diff --git a/src/plugins/gfi/utils/updateTooltip.ts b/src/plugins/gfi/utils/updateTooltip.ts
index 7ed1fe73ce..b75d2dccd4 100644
--- a/src/plugins/gfi/utils/updateTooltip.ts
+++ b/src/plugins/gfi/utils/updateTooltip.ts
@@ -1,4 +1,4 @@
-import { Feature, Overlay, type MapBrowserEvent } from 'ol'
+import { Feature, type MapBrowserEvent, Overlay } from 'ol'
import { getTooltip } from '@/lib/tooltip'
diff --git a/src/plugins/iconMenu/index.ts b/src/plugins/iconMenu/index.ts
index 84584d0901..9ad576f24a 100644
--- a/src/plugins/iconMenu/index.ts
+++ b/src/plugins/iconMenu/index.ts
@@ -9,7 +9,7 @@ import type { PluginContainer, PolarPluginStore } from '@/core'
import component from './components/IconMenu.ce.vue'
import locales from './locales'
import { useIconMenuStore } from './store'
-import { PluginId, type IconMenuPluginOptions } from './types'
+import { type IconMenuPluginOptions, PluginId } from './types'
/**
* Creates a plugin which adds the possibility to open various functionality as
diff --git a/src/plugins/layerChooser/index.ts b/src/plugins/layerChooser/index.ts
index c0a6d5516f..5fd26f2d70 100644
--- a/src/plugins/layerChooser/index.ts
+++ b/src/plugins/layerChooser/index.ts
@@ -5,8 +5,8 @@
/* eslint-enable tsdoc/syntax */
import type {
- PluginContainer,
InterfacePluginOptions,
+ PluginContainer,
PolarPluginStore,
} from '@/core'
diff --git a/src/plugins/pins/index.ts b/src/plugins/pins/index.ts
index 8f3e934b8d..b0508ac1cf 100644
--- a/src/plugins/pins/index.ts
+++ b/src/plugins/pins/index.ts
@@ -8,7 +8,7 @@ import type { PluginContainer, PolarPluginStore } from '@/core'
import locales from './locales'
import { usePinsStore } from './store'
-import { PluginId, type PinsPluginOptions } from './types'
+import { type PinsPluginOptions, PluginId } from './types'
/**
* Pins plugin for POLAR that adds map interactions to client that allow users
diff --git a/src/plugins/pins/utils/getPinStyle.ts b/src/plugins/pins/utils/getPinStyle.ts
index 8bc8101bbd..5528490af5 100644
--- a/src/plugins/pins/utils/getPinStyle.ts
+++ b/src/plugins/pins/utils/getPinStyle.ts
@@ -1,4 +1,4 @@
-import { Style, Icon } from 'ol/style'
+import { Icon, Style } from 'ol/style'
import type { PinStyle } from '../types'
diff --git a/src/plugins/scale/index.ts b/src/plugins/scale/index.ts
index 48b4d33942..1cd76b8ae6 100644
--- a/src/plugins/scale/index.ts
+++ b/src/plugins/scale/index.ts
@@ -9,7 +9,7 @@ import type { PluginContainer, PolarPluginStore } from '@/core'
import component from './components/ScaleWidget.ce.vue'
import locales from './locales'
import { useScaleStore } from './store'
-import { type ScalePluginOptions, PluginId } from './types'
+import { PluginId, type ScalePluginOptions } from './types'
export { beautifyScale } from './utils/beautifyScale'
export { calculateScaleFromResolution } from './utils/calculateScaleFromResolution'
diff --git a/src/plugins/scale/store.ts b/src/plugins/scale/store.ts
index d6c97093db..82bc0cc2da 100644
--- a/src/plugins/scale/store.ts
+++ b/src/plugins/scale/store.ts
@@ -6,7 +6,7 @@
import { t } from 'i18next'
import { acceptHMRUpdate, defineStore } from 'pinia'
-import { ref, computed, onScopeDispose } from 'vue'
+import { computed, onScopeDispose, ref } from 'vue'
import { useCoreStore } from '@/core/stores'
import { computedT } from '@/lib/computedT'
diff --git a/src/plugins/toast/components/ToastUI.spec.ts b/src/plugins/toast/components/ToastUI.spec.ts
index 968461357b..aad13cf893 100644
--- a/src/plugins/toast/components/ToastUI.spec.ts
+++ b/src/plugins/toast/components/ToastUI.spec.ts
@@ -1,6 +1,6 @@
import { createTestingPinia } from '@pinia/testing'
import { mount, VueWrapper } from '@vue/test-utils'
-import { expect, test as _test, vi } from 'vitest'
+import { test as _test, expect, vi } from 'vitest'
import { nextTick } from 'vue'
import { mockedT } from '@/test/utils/mockI18n'
diff --git a/src/plugins/toast/store.ts b/src/plugins/toast/store.ts
index ef48711492..f3e69f8975 100644
--- a/src/plugins/toast/store.ts
+++ b/src/plugins/toast/store.ts
@@ -6,7 +6,7 @@
import { toMerged } from 'es-toolkit'
import { acceptHMRUpdate, defineStore } from 'pinia'
-import { computed, ref, toRaw, type Reactive } from 'vue'
+import { computed, type Reactive, ref, toRaw } from 'vue'
import { useCoreStore } from '@/core/stores'
diff --git a/vite.config.ts b/vite.config.ts
index 6f498ed1fa..357a15483d 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,7 +1,7 @@
import vue from '@vitejs/plugin-vue'
import { globSync } from 'node:fs'
import { createRequire } from 'node:module'
-import { resolve, basename, sep } from 'node:path'
+import { basename, resolve, sep } from 'node:path'
import { defineConfig } from 'vite'
import checker from 'vite-plugin-checker'
import commonJs from 'vite-plugin-commonjs'
From f46aa79097a4a291b40ee3f021cea2b79ec0f3a6 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Wed, 13 May 2026 13:58:35 +0200
Subject: [PATCH 45/47] chore(gfi): remove migrated type HighlightStyle from
vue2 code
---
vue2/packages/types/custom/core.ts | 6 ------
1 file changed, 6 deletions(-)
diff --git a/vue2/packages/types/custom/core.ts b/vue2/packages/types/custom/core.ts
index 9a5049082a..d04aeae4b6 100644
--- a/vue2/packages/types/custom/core.ts
+++ b/vue2/packages/types/custom/core.ts
@@ -168,12 +168,6 @@ export interface FilterConfiguration extends PluginOptions {
>
}
-/** Object containing information for highlighting a gfi result */
-export interface HighlightStyle {
- fill: Fill
- stroke: Stroke
-}
-
export interface LayerChooserConfiguration extends PluginOptions {
component?: VueConstructor
}
From b8fb5a2a311d6ab445ef7fc97088a8da3263be14 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Wed, 13 May 2026 14:00:13 +0200
Subject: [PATCH 46/47] fix(gfi): add missing import of PluginId to GfiUI
---
src/plugins/gfi/components/GfiUI.ce.vue | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/plugins/gfi/components/GfiUI.ce.vue b/src/plugins/gfi/components/GfiUI.ce.vue
index 3915c69b66..5b20739820 100644
--- a/src/plugins/gfi/components/GfiUI.ce.vue
+++ b/src/plugins/gfi/components/GfiUI.ce.vue
@@ -39,6 +39,7 @@ import PolarCard from '@/components/PolarCard.ce.vue'
import { useCoreStore } from '@/core/stores'
import { useGfiStore } from '../store'
+import { PluginId } from '../types'
import GfiFeature from './GfiFeature.ce.vue'
import GfiFeatureList from './GfiFeatureList.ce.vue'
From 985bc029ff84dd1e494b3140c58678751b7a0af5 Mon Sep 17 00:00:00 2001
From: Hendrik Oenings
Date: Wed, 13 May 2026 14:07:23 +0200
Subject: [PATCH 47/47] fix: using more KERN variables for KernPagination
---
src/components/kern/KernPagination.ce.vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/kern/KernPagination.ce.vue b/src/components/kern/KernPagination.ce.vue
index d1673c1713..372c39cd89 100644
--- a/src/components/kern/KernPagination.ce.vue
+++ b/src/components/kern/KernPagination.ce.vue
@@ -152,13 +152,13 @@ ul {
pointer-events: none;
.kern-label {
- color: white;
+ color: var(--kern-color-action-on-default);
}
}
.kern-label {
font-size: var(--kern-typography-font-size-static-small);
- line-height: var(--kern-typography-line-height-static-small);
+ line-height: var(--kern-typography-line-height-static-medium);
}
}