Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/core/stores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,37 @@ export const useCoreStore = defineStore('core', () => {
*/
zoom: mainStoreRefs.zoom,

/**
* Masks an interaction for a plugin.
* If the interaction is already masked by another plugin, an error is thrown.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if I start the draw plugin and want to draw a polygon, then decide that I want to instead find a route but forget to turn of the draw plugin - maskInteraction fails in routing with an error but the user does not receive an information about that. That doesn't really seem the way to go.

This can currently be tested if one removes the if-statement in maskInteraction and then clicking on one of the input-fields of the routing plugin.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected behaviour is that I can activate the next plugin s.t. the previous is deactivated then. This is implemented w/ 5fb2060

*
* This may, for example, be used for interactions that should not be triggered while drawing.
*
* @param pluginId - ID of the plugin that wants to mask the interaction
* @param interaction - Name of the interaction to be masked
* @alpha
*/
maskInteraction: mainStore.maskInteraction,

/**
* Unmasks an interaction for a plugin.
* If the interaction is not masked by the plugin, nothing happens.
*
* @param pluginId - ID of the plugin that wants to unmask the interaction
* @param interaction - Name of the interaction to be unmasked
* @alpha
*/
unmaskInteraction: mainStore.unmaskInteraction,

/**
* Checks whether an interaction is masked by another plugin.
*
* @param interaction - Name of the interaction to be checked
* @returns `true` if the interaction is masked by another plugin, `false` otherwise
* @alpha
*/
isInteractionMasked: mainStore.isInteractionMasked,

/**
* Before instantiating the map, all required plugins have to be added. Depending on how you use POLAR, this may
* already have been done. Ready-made clients (that is, packages prefixed `@polar/client-`) come with plugins prepared.
Expand Down
97 changes: 97 additions & 0 deletions src/core/stores/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
ColorScheme,
MapConfigurationIncludingDefaults,
MasterportalApiServiceRegister,
PluginId,
} from '../types'

import { rawLayerList } from '@masterportal/masterportalapi'
Expand Down Expand Up @@ -35,7 +36,7 @@

const layout = computed(() => configuration.value.layout ?? 'nineRegions')

// TODO(dopenguin): Both will possibly be updated with different breakpoints -> Breakpoints are e.g. not valid on newer devices

Check warning on line 39 in src/core/stores/main.ts

View workflow job for this annotation

GitHub Actions / Linting

Unexpected 'todo' comment: 'TODO(dopenguin): Both will possibly be...'
const clientHeight = ref(0)
const clientWidth = ref(0)
const hasSmallHeight = computed(
Expand Down Expand Up @@ -89,6 +90,31 @@
return { ...register, ...polar } as typeof polar
}

const maskedInteractions = ref(
new globalThis.Map<string, { pluginId: PluginId; teardown: () => void }>()
)
function maskInteraction(
pluginId: PluginId,
interaction: string,
setup: () => void,
teardown: () => void
) {
if (maskedInteractions.value.has(interaction)) {
maskedInteractions.value.get(interaction)?.teardown()
}
maskedInteractions.value.set(interaction, { pluginId, teardown })
setup()
}
function unmaskInteraction(pluginId: PluginId, interaction: string) {
if (maskedInteractions.value.get(interaction)?.pluginId === pluginId) {
maskedInteractions.value.get(interaction)?.teardown()
maskedInteractions.value.delete(interaction)
}
}
function isInteractionMasked(interaction: string) {
return maskedInteractions.value.has(interaction)
}

function setup() {
addEventListener('resize', updateHasSmallDisplay)
updateHasSmallDisplay()
Expand Down Expand Up @@ -124,6 +150,9 @@
centerOnFeature,
updateHasSmallDisplay,
getLayerMapConfiguration,
maskInteraction,
unmaskInteraction,
isInteractionMasked,
setup,
teardown,
}
Expand All @@ -132,3 +161,71 @@
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useMainStore, import.meta.hot))
}

if (import.meta.vitest) {
const { vi, expect, test: _test } = import.meta.vitest
const { createPinia, setActivePinia } = await import('pinia')

/* eslint-disable no-empty-pattern */
const test = _test.extend<{
store: ReturnType<typeof useMainStore>
}>({
store: async ({}, use) => {
setActivePinia(createPinia())
const store = useMainStore()
store.setup()
await use(store)
store.teardown()
},
})
/* eslint-enable no-empty-pattern */

test('interactions can be masked and unmasked', ({ store }) => {
const pluginId = 'external-test-plugin'
const interaction = 'click'
const setup = vi.fn()
const teardown = vi.fn()

expect(store.isInteractionMasked(interaction)).toBe(false)

store.maskInteraction(pluginId, interaction, setup, teardown)
expect(store.isInteractionMasked(interaction)).toBe(true)
expect(setup).toHaveBeenCalledTimes(1)
expect(teardown).toHaveBeenCalledTimes(0)

store.unmaskInteraction(pluginId, interaction)
expect(store.isInteractionMasked(interaction)).toBe(false)
expect(setup).toHaveBeenCalledTimes(1)
expect(teardown).toHaveBeenCalledTimes(1)
})

test('masking the same interaction twice tears down the previous mask', ({
store,
}) => {
const interaction = 'click'
const firstPluginId = 'external-test-plugin'
const firstSetup = vi.fn()
const firstTeardown = vi.fn()
const secondPluginId = 'external-second-test-plugin'
const secondSetup = vi.fn()
const secondTeardown = vi.fn()

expect(store.isInteractionMasked(interaction)).toBe(false)

store.maskInteraction(firstPluginId, interaction, firstSetup, firstTeardown)
expect(store.isInteractionMasked(interaction)).toBe(true)
expect(firstSetup).toHaveBeenCalledTimes(1)
expect(firstTeardown).toHaveBeenCalledTimes(0)

store.maskInteraction(
secondPluginId,
interaction,
secondSetup,
secondTeardown
)
expect(store.isInteractionMasked(interaction)).toBe(true)
expect(firstTeardown).toHaveBeenCalledTimes(1)
expect(secondSetup).toHaveBeenCalledTimes(1)
expect(secondTeardown).toHaveBeenCalledTimes(0)
})
}
20 changes: 2 additions & 18 deletions src/plugins/pins/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { PinMovable, PinsPluginOptions } from './types'

import { toMerged } from 'es-toolkit'
import { pointerMove } from 'ol/events/condition'
import { Draw, Modify, Select, Translate } from 'ol/interaction'
import { Select, Translate } from 'ol/interaction'
import { toLonLat } from 'ol/proj'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
Expand Down Expand Up @@ -167,28 +167,12 @@ export const usePinsStore = defineStore('plugins/pins', () => {
}

async function click(coordinate: Coordinate) {
const isDrawing = coreStore.map
.getInteractions()
.getArray()
.some(
(interaction) =>
(interaction instanceof Draw &&
// @ts-expect-error | internal hack to detect it from gfi plugin
(interaction._isMultiSelect ||
// @ts-expect-error | internal hack to detect it from routing plugin
interaction._isRoutingDraw ||
// @ts-expect-error | internal hack to detect it from draw plugin
interaction._isDrawPlugin)) ||
interaction instanceof Modify ||
// @ts-expect-error | internal hack to detect it from draw plugin
interaction._isDeleteSelect
)
const { minZoomLevel, movable } = configuration.value
if (
(movable === 'drag' || movable === 'click') &&
// NOTE: It is assumed that getZoom actually returns the currentZoomLevel, thus the view has a constraint in the resolution.
(coreStore.map.getView().getZoom() as number) >= minZoomLevel &&
!isDrawing &&
!coreStore.isInteractionMasked('click') &&
(await isCoordinateInBoundaryLayer(
coordinate,
coreStore.map,
Expand Down
20 changes: 13 additions & 7 deletions src/plugins/routing/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,18 @@ export const useRoutingStore = defineStore('plugins/routing', () => {
_currentlyFocusedInput.value = index

if (index !== -1) {
coreStore.map.addInteraction(draw as Draw)
coreStore.maskInteraction(
'routing',
'click',
() => {
coreStore.map.addInteraction(draw as Draw)
},
() => {
coreStore.map.removeInteraction(draw as Draw)
}
)
} else {
coreStore.map.removeInteraction(draw as Draw)
coreStore.unmaskInteraction('routing', 'click')
}
},
})
Expand Down Expand Up @@ -221,12 +230,9 @@ export const useRoutingStore = defineStore('plugins/routing', () => {

function initializeDraw() {
draw = new Draw({ stopClick: true, type: 'Point' })
// @ts-expect-error | internal hack to detect it in @polar/plugin-pins and @polar/plugin-gfi
draw._isRoutingDraw = true
draw.on('drawend', (e) => {
addCoordinateToRoute((e.feature.getGeometry() as Point).getCoordinates())
// @ts-expect-error | internal hack to detect it in @polar/plugin-pins and @polar/plugin-gfi
draw._isRoutingDraw = false
coreStore.unmaskInteraction('routing', 'click')
currentlyFocusedInput.value = -1
})
}
Expand Down Expand Up @@ -293,7 +299,7 @@ export const useRoutingStore = defineStore('plugins/routing', () => {
reset()

if (draw) {
coreStore.map.removeInteraction(draw)
coreStore.unmaskInteraction('routing', 'click')
draw = undefined
}
}
Expand Down
Loading