@defra/interactive-map 0.0.21-alpha → 0.0.23-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +39 -0
- package/docs/api/context.md +4 -4
- package/docs/api/map-style-config.md +11 -2
- package/docs/api/marker-config.md +38 -7
- package/docs/api/panel-definition.md +7 -3
- package/docs/api/symbol-config.md +10 -26
- package/docs/api/symbol-registry.md +28 -13
- package/docs/api.md +65 -4
- package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
- package/docs/assets/images/toggle-marker-label.jpg +0 -0
- package/docs/examples/index.mdx +12 -0
- package/docs/examples/toggle-marker-label.mdx +89 -0
- package/docs/plugins/datasets.md +39 -1
- package/docs/plugins/interact.md +4 -11
- package/docusaurus.config.cjs +2 -1
- package/package.json +10 -12
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -0
- package/plugins/beta/datasets/dist/css/index.css +2 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +2 -0
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +2 -0
- package/plugins/beta/datasets/dist/umd/maplibre/index.js +2 -0
- package/plugins/beta/datasets/src/adapters/maplibre/index.js +1 -1
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
- package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
- package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
- package/plugins/beta/datasets/src/index.maplibre.umd.js +23 -0
- package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +1 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +23 -55
- package/plugins/interact/src/InteractInit.test.js +58 -80
- package/plugins/interact/src/defaults.js +1 -3
- package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +65 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
- package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
- package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
- package/plugins/interact/src/hooks/useMapItemList.js +187 -0
- package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
- package/plugins/interact/src/index.js +0 -2
- package/plugins/interact/src/index.test.js +0 -2
- package/plugins/interact/src/manifest.js +37 -6
- package/plugins/interact/src/reducer.js +8 -4
- package/plugins/interact/src/reducer.test.js +18 -3
- package/plugins/interact/src/utils/buildStylesMap.js +21 -10
- package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
- package/plugins/interact/src/utils/spatial.js +0 -15
- package/plugins/interact/src/utils/spatial.test.js +1 -13
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/components/CloseButton/CloseButton.module.scss +2 -0
- package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
- package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
- package/plugins/search/src/search.scss +1 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +4 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/index.js +7 -1
- package/providers/maplibre/src/index.test.js +15 -2
- package/providers/maplibre/src/mapEvents.js +11 -5
- package/providers/maplibre/src/mapEvents.test.js +14 -3
- package/providers/maplibre/src/maplibreProvider.js +18 -11
- package/providers/maplibre/src/maplibreProvider.test.js +43 -36
- package/providers/maplibre/src/utils/highlightFeatures.js +102 -57
- package/providers/maplibre/src/utils/highlightFeatures.test.js +162 -51
- package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
- package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
- package/providers/maplibre/src/utils/patternImages.js +10 -12
- package/providers/maplibre/src/utils/patternImages.test.js +145 -167
- package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
- package/providers/maplibre/src/utils/symbolImages.js +23 -86
- package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
- package/rollup.esm.mjs +1 -1
- package/src/App/components/Actions/Actions.module.scss +1 -0
- package/src/App/components/Actions/Actions.test.jsx +17 -4
- package/src/App/components/Hints/Hints.jsx +52 -0
- package/src/App/components/Hints/Hints.module.scss +37 -0
- package/src/App/components/Hints/Hints.test.jsx +104 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
- package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
- package/src/App/components/Markers/LabelMarker.jsx +14 -0
- package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
- package/src/App/components/Markers/Markers.jsx +44 -75
- package/src/App/components/Markers/Markers.module.scss +58 -0
- package/src/App/components/Markers/Markers.test.jsx +90 -113
- package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
- package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
- package/src/App/components/Markers/SymbolMarker.jsx +31 -0
- package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
- package/src/App/components/Panel/Panel.jsx +1 -1
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
- package/src/App/components/Tabs/Tabs.jsx +68 -0
- package/src/App/components/Tabs/Tabs.module.scss +57 -0
- package/src/App/components/Tabs/Tabs.test.jsx +121 -0
- package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
- package/src/App/components/Viewport/Features.jsx +35 -0
- package/src/App/components/Viewport/Features.module.scss +31 -0
- package/src/App/components/Viewport/Features.test.jsx +124 -0
- package/src/App/components/Viewport/MapController.jsx +1 -1
- package/src/App/components/Viewport/MapStatus.jsx +0 -4
- package/src/App/components/Viewport/Viewport.jsx +35 -29
- package/src/App/components/Viewport/Viewport.module.scss +7 -21
- package/src/App/components/Viewport/Viewport.test.jsx +108 -65
- package/src/App/controls/keyboardActions.js +9 -3
- package/src/App/controls/keyboardActions.test.js +122 -87
- package/src/App/controls/keyboardShortcuts.js +1 -14
- package/src/App/hooks/useFeatureFocus.js +199 -0
- package/src/App/hooks/useFeatureFocus.test.js +635 -0
- package/src/App/hooks/useFeatureItems.js +30 -0
- package/src/App/hooks/useFeatureItems.test.js +97 -0
- package/src/App/hooks/useFocusVisible.js +4 -7
- package/src/App/hooks/useFocusVisible.test.js +6 -6
- package/src/App/hooks/useKeyboardHint.js +9 -34
- package/src/App/hooks/useKeyboardHint.test.js +34 -84
- package/src/App/hooks/useKeyboardShortcuts.js +8 -5
- package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
- package/src/App/hooks/useLayoutMeasurements.js +10 -4
- package/src/App/hooks/useLayoutMeasurements.test.js +30 -0
- package/src/App/hooks/useMapURLSync.js +3 -3
- package/src/App/hooks/useMapURLSync.test.js +20 -3
- package/src/App/hooks/useMarkerCursor.js +33 -0
- package/src/App/hooks/useMarkerCursor.test.js +87 -0
- package/src/App/hooks/useMarkersAPI.js +32 -9
- package/src/App/hooks/useMarkersAPI.test.js +162 -94
- package/src/App/layout/Layout.jsx +2 -0
- package/src/App/layout/Layout.test.jsx +3 -0
- package/src/App/registry/keyboardShortcutRegistry.js +3 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
- package/src/App/renderer/mapButtons.js +5 -1
- package/src/App/renderer/mapButtons.test.js +37 -1
- package/src/App/store/ServiceProvider.jsx +4 -1
- package/src/App/store/ServiceProvider.test.jsx +11 -0
- package/src/App/store/appActionsMap.js +5 -1
- package/src/App/store/appActionsMap.test.js +13 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +13 -1
- package/src/InteractiveMap/InteractiveMap.test.js +6 -4
- package/src/InteractiveMap/behaviourController.js +1 -2
- package/src/InteractiveMap/buttonManager.js +1 -3
- package/src/InteractiveMap/buttonManager.test.js +16 -8
- package/src/InteractiveMap/domStateManager.js +2 -3
- package/src/InteractiveMap/historyManager.js +4 -3
- package/src/config/appConfig.js +3 -1
- package/src/config/appConfig.test.js +4 -0
- package/src/config/defaults.js +3 -2
- package/src/config/events.js +8 -0
- package/src/config/mapTheme.js +8 -4
- package/src/config/mergeConfig.js +8 -2
- package/src/config/mergeConfig.test.js +19 -0
- package/src/config/symbolConfig.js +23 -16
- package/src/scss/main.scss +3 -0
- package/src/scss/tools/_border-focus.scss +0 -1
- package/src/services/eventBus.js +15 -0
- package/src/services/eventBus.test.js +11 -0
- package/src/services/hints.js +47 -0
- package/src/services/hints.test.js +110 -0
- package/src/services/patternRegistry.js +69 -1
- package/src/services/patternRegistry.test.js +112 -1
- package/src/services/reverseGeocode.js +4 -0
- package/src/services/reverseGeocode.test.js +11 -1
- package/src/services/symbolRegistry.js +152 -17
- package/src/services/symbolRegistry.test.js +141 -31
- package/src/types.js +93 -16
- package/src/utils/constrainKeyboardFocus.js +5 -5
- package/src/utils/detectInterfaceType.js +27 -5
- package/src/utils/detectInterfaceType.test.js +29 -3
- package/src/utils/mapStateSync.js +2 -2
- package/src/utils/mapStateSync.test.js +18 -0
- package/src/utils/patternUtils.js +4 -67
- package/src/utils/patternUtils.test.js +1 -123
- package/src/utils/symbolUtils.js +11 -13
- package/src/utils/symbolUtils.test.js +18 -26
- package/webpack.dev.mjs +4 -0
- package/webpack.umd.mjs +9 -6
- package/plugins/beta/datasets/adapters/maplibre/dist/esm/im-datasets-ml-adapter.js +0 -1
- package/plugins/beta/datasets/adapters/maplibre/dist/umd/im-datasets-ml-adapter.js +0 -2
- package/plugins/beta/datasets/adapters/maplibre/dist/umd/index.js +0 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +0 -2
- package/plugins/beta/datasets/dist/umd/index.js +0 -2
- package/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
- /package/plugins/beta/datasets/{adapters/maplibre/dist → dist/adapters/maplibre}/esm/index.js +0 -0
- /package/plugins/beta/datasets/{adapters/maplibre/dist/umd/im-datasets-ml-adapter.js.LICENSE.txt → dist/umd/maplibre/im-datasets-maplibre-adapter.js.LICENSE.txt} +0 -0
- /package/plugins/beta/datasets/dist/umd/{im-datasets-plugin.js.LICENSE.txt → maplibre/im-datasets-plugin.js.LICENSE.txt} +0 -0
- /package/plugins/beta/datasets/dist/umd/{index.js.LICENSE.txt → maplibre/index.js.LICENSE.txt} +0 -0
package/src/scss/main.scss
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
// Components: specific UI components. Using 'CSS in Component' pattern
|
|
19
19
|
@use '../App/components/KeyboardHelp/KeyboardHelp.module';
|
|
20
|
+
@use '../App/components/Tabs/Tabs.module';
|
|
20
21
|
@use '../App/components/MapButton/MapButton.module';
|
|
21
22
|
@use '../App/components/Panel/Panel.module';
|
|
22
23
|
@use '../App/components/Icon/Icon.module';
|
|
@@ -25,8 +26,10 @@
|
|
|
25
26
|
@use '../App/components/PopupMenu/PopupMenu.module';
|
|
26
27
|
@use '../App/components/Attributions/Attributions.module';
|
|
27
28
|
@use '../App/components/Logo/Logo.module';
|
|
29
|
+
@use '../App/components/Hints/Hints.module';
|
|
28
30
|
@use '../App/components/CrossHair/CrossHair.module';
|
|
29
31
|
@use '../App/components/Markers/Markers.module';
|
|
32
|
+
@use '../App/components/Viewport/Features.module';
|
|
30
33
|
@use '../App/components/Actions/Actions.module';
|
|
31
34
|
|
|
32
35
|
// Components that dont have a corresponding React Component
|
package/src/services/eventBus.js
CHANGED
|
@@ -31,6 +31,21 @@ class EventBus {
|
|
|
31
31
|
return this
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Register a one-time handler that removes itself after the first call.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} eventName
|
|
38
|
+
* @param {Function} handler
|
|
39
|
+
* @returns {this}
|
|
40
|
+
*/
|
|
41
|
+
once (eventName, handler) {
|
|
42
|
+
const wrapper = (...args) => {
|
|
43
|
+
this.off(eventName, wrapper)
|
|
44
|
+
handler(...args)
|
|
45
|
+
}
|
|
46
|
+
return this.on(eventName, wrapper)
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
/**
|
|
35
50
|
* Remove a handler for an event. Omit `handler` to remove all handlers.
|
|
36
51
|
*
|
|
@@ -82,6 +82,17 @@ describe('EventBus singleton', () => {
|
|
|
82
82
|
console.error.mockRestore()
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
it('once fires the handler exactly once', () => {
|
|
86
|
+
const handler = jest.fn()
|
|
87
|
+
eventBus.once('single', handler)
|
|
88
|
+
|
|
89
|
+
eventBus.emit('single', 'a')
|
|
90
|
+
eventBus.emit('single', 'b')
|
|
91
|
+
|
|
92
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
93
|
+
expect(handler).toHaveBeenCalledWith('a')
|
|
94
|
+
})
|
|
95
|
+
|
|
85
96
|
it('destroys all events', () => {
|
|
86
97
|
const handler = jest.fn()
|
|
87
98
|
eventBus.on('destroyMe', handler)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const stripHtml = (html) => html.replace(/<[^<>]*>/g, '')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates the hints service. Manages a single active toast hint shown in the
|
|
5
|
+
* Hints container. Calling show() replaces any current hint and
|
|
6
|
+
* restarts the dismiss timer. Internally calls announce() so screen readers
|
|
7
|
+
* receive the message through the live region without callers needing to pair
|
|
8
|
+
* the two calls manually.
|
|
9
|
+
*/
|
|
10
|
+
export function createHints (announce) {
|
|
11
|
+
const subscribers = new Set()
|
|
12
|
+
let current = null
|
|
13
|
+
let timer = null
|
|
14
|
+
|
|
15
|
+
const notify = () => subscribers.forEach(fn => fn(current))
|
|
16
|
+
|
|
17
|
+
const clearTimer = () => {
|
|
18
|
+
if (timer) {
|
|
19
|
+
clearTimeout(timer)
|
|
20
|
+
timer = null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dismiss = () => {
|
|
25
|
+
clearTimer()
|
|
26
|
+
current = null
|
|
27
|
+
notify()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const show = (html, options = {}) => {
|
|
31
|
+
const { duration = 4000, announce: announceText } = options
|
|
32
|
+
clearTimer()
|
|
33
|
+
current = { html }
|
|
34
|
+
notify()
|
|
35
|
+
announce(announceText ?? stripHtml(html), 'plugin')
|
|
36
|
+
if (duration > 0) {
|
|
37
|
+
timer = setTimeout(dismiss, duration)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const subscribe = (fn) => {
|
|
42
|
+
subscribers.add(fn)
|
|
43
|
+
return () => subscribers.delete(fn)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { show, dismiss, subscribe }
|
|
47
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHints } from './hints.js'
|
|
2
|
+
|
|
3
|
+
let announce
|
|
4
|
+
let hints
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
jest.useFakeTimers()
|
|
8
|
+
announce = jest.fn()
|
|
9
|
+
hints = createHints(announce)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
jest.useRealTimers()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// ─── show() ──────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe('show', () => {
|
|
19
|
+
it('notifies subscribers with the html payload', () => {
|
|
20
|
+
const sub = jest.fn()
|
|
21
|
+
hints.subscribe(sub)
|
|
22
|
+
hints.show('<kbd>Enter</kbd> to select')
|
|
23
|
+
expect(sub).toHaveBeenCalledWith({ html: '<kbd>Enter</kbd> to select' })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('calls announce with stripped html', () => {
|
|
27
|
+
hints.show('<kbd>Enter</kbd> to select')
|
|
28
|
+
expect(announce).toHaveBeenCalledWith('Enter to select', 'plugin')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('calls announce with custom text when announce option is provided', () => {
|
|
32
|
+
hints.show('<kbd>Alt+K</kbd> help', { announce: 'Press Alt+K for keyboard controls' })
|
|
33
|
+
expect(announce).toHaveBeenCalledWith('Press Alt+K for keyboard controls', 'plugin')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('replaces an existing hint and resets the timer', () => {
|
|
37
|
+
const sub = jest.fn()
|
|
38
|
+
hints.subscribe(sub)
|
|
39
|
+
hints.show('first', { duration: 2000 })
|
|
40
|
+
hints.show('second', { duration: 2000 })
|
|
41
|
+
jest.advanceTimersByTime(2000)
|
|
42
|
+
expect(sub).toHaveBeenLastCalledWith(null)
|
|
43
|
+
// Only one dismiss fired (the second hint's timer)
|
|
44
|
+
expect(sub).toHaveBeenCalledTimes(3) // show1, show2, dismiss
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('auto-dismisses after duration', () => {
|
|
48
|
+
const sub = jest.fn()
|
|
49
|
+
hints.subscribe(sub)
|
|
50
|
+
hints.show('hello', { duration: 3000 })
|
|
51
|
+
jest.advanceTimersByTime(3000)
|
|
52
|
+
expect(sub).toHaveBeenLastCalledWith(null)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('does not auto-dismiss when duration is 0', () => {
|
|
56
|
+
const sub = jest.fn()
|
|
57
|
+
hints.subscribe(sub)
|
|
58
|
+
hints.show('persistent', { duration: 0 })
|
|
59
|
+
jest.advanceTimersByTime(60000)
|
|
60
|
+
expect(sub).not.toHaveBeenCalledWith(null)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ─── dismiss() ───────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
describe('dismiss', () => {
|
|
67
|
+
it('clears the active hint and notifies', () => {
|
|
68
|
+
const sub = jest.fn()
|
|
69
|
+
hints.subscribe(sub)
|
|
70
|
+
hints.show('hello')
|
|
71
|
+
hints.dismiss()
|
|
72
|
+
expect(sub).toHaveBeenLastCalledWith(null)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('cancels the auto-dismiss timer', () => {
|
|
76
|
+
const sub = jest.fn()
|
|
77
|
+
hints.subscribe(sub)
|
|
78
|
+
hints.show('hello', { duration: 3000 })
|
|
79
|
+
hints.dismiss()
|
|
80
|
+
sub.mockClear()
|
|
81
|
+
jest.advanceTimersByTime(3000)
|
|
82
|
+
expect(sub).not.toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('is safe to call when no hint is active', () => {
|
|
86
|
+
expect(() => hints.dismiss()).not.toThrow()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// ─── subscribe / unsubscribe ──────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
describe('subscribe', () => {
|
|
93
|
+
it('returns an unsubscribe function', () => {
|
|
94
|
+
const sub = jest.fn()
|
|
95
|
+
const unsub = hints.subscribe(sub)
|
|
96
|
+
unsub()
|
|
97
|
+
hints.show('hello')
|
|
98
|
+
expect(sub).not.toHaveBeenCalled()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('notifies multiple subscribers', () => {
|
|
102
|
+
const sub1 = jest.fn()
|
|
103
|
+
const sub2 = jest.fn()
|
|
104
|
+
hints.subscribe(sub1)
|
|
105
|
+
hints.subscribe(sub2)
|
|
106
|
+
hints.show('hello')
|
|
107
|
+
expect(sub1).toHaveBeenCalled()
|
|
108
|
+
expect(sub2).toHaveBeenCalled()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BUILT_IN_PATTERNS } from '../config/patternConfig.js'
|
|
2
|
-
|
|
2
|
+
import { getValueForStyle } from '../utils/getValueForStyle.js'
|
|
3
|
+
import { KEY_BORDER_PATH, getEffectivePixelRatio, injectColors, hashString } from '../utils/patternUtils.js'
|
|
3
4
|
const patterns = new Map()
|
|
4
5
|
|
|
5
6
|
export const patternRegistry = {
|
|
@@ -31,6 +32,73 @@ export const patternRegistry = {
|
|
|
31
32
|
*/
|
|
32
33
|
list () {
|
|
33
34
|
return [...patterns.values()]
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Clears all registered patterns (including built-ins). Mainly for testing purposes.
|
|
39
|
+
*/
|
|
40
|
+
clear () {
|
|
41
|
+
patterns.clear()
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the raw (un-coloured) inner SVG content for a style's pattern.
|
|
46
|
+
* Precedence: inline fillPatternSvgContent → named fillPattern from registry.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} style
|
|
49
|
+
* @returns {string|null}
|
|
50
|
+
*/
|
|
51
|
+
getPatternInnerContent (style) {
|
|
52
|
+
if (style.fillPatternSvgContent) {
|
|
53
|
+
return style.fillPatternSvgContent
|
|
54
|
+
}
|
|
55
|
+
if (style.fillPattern) {
|
|
56
|
+
return this.get(style.fillPattern)?.svgContent ?? null
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns colour-injected SVG path content for use in Key panel pattern symbols.
|
|
63
|
+
* Returns { border, content } where border is the rounded-rect outline and content
|
|
64
|
+
* is the pattern fill. Returns null if the style has no pattern.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} style
|
|
67
|
+
* @param {string} mapStyleId
|
|
68
|
+
* @returns {{ border: string, content: string }|null}
|
|
69
|
+
*/
|
|
70
|
+
getKeyPatternPaths (style, mapStyleId) {
|
|
71
|
+
const innerContent = this.getPatternInnerContent(style)
|
|
72
|
+
if (!innerContent) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
const fg = getValueForStyle(style.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
76
|
+
const bg = getValueForStyle(style.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
77
|
+
const borderStroke = getValueForStyle(style.stroke, mapStyleId) || fg
|
|
78
|
+
const keyPatternPaths = {
|
|
79
|
+
border: injectColors(KEY_BORDER_PATH, borderStroke, bg),
|
|
80
|
+
content: injectColors(innerContent, fg, bg)
|
|
81
|
+
}
|
|
82
|
+
return keyPatternPaths
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns a deterministic image ID for a pattern + resolved colour + pixel ratio combination.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} dataset
|
|
89
|
+
* @param {string} mapStyleId
|
|
90
|
+
* @param {number} [pixelRatio=1]
|
|
91
|
+
* @returns {string|null}
|
|
92
|
+
*/
|
|
93
|
+
getPatternImageId (dataset, mapStyleId, pixelRatio = 1) {
|
|
94
|
+
const innerContent = this.getPatternInnerContent(dataset)
|
|
95
|
+
if (!innerContent) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
99
|
+
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
100
|
+
const effectiveRatio = getEffectivePixelRatio(pixelRatio)
|
|
101
|
+
return `pattern-${hashString(innerContent + fg + bg)}-${effectiveRatio}x`
|
|
34
102
|
}
|
|
35
103
|
}
|
|
36
104
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { patternRegistry } from './patternRegistry.js'
|
|
2
|
-
|
|
2
|
+
import { KEY_BORDER_PATH } from '../utils/patternUtils.js'
|
|
3
3
|
describe('patternRegistry', () => {
|
|
4
4
|
describe('built-in patterns', () => {
|
|
5
5
|
test.each([
|
|
@@ -45,4 +45,115 @@ describe('patternRegistry', () => {
|
|
|
45
45
|
expect(all.every(p => p.id && p.svgContent)).toBe(true)
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
describe('getPatternInnerContent', () => {
|
|
50
|
+
test('returns fillPatternSvgContent when set (inline SVG takes precedence)', () => {
|
|
51
|
+
const style = { fillPatternSvgContent: '<path d="custom"/>', fillPattern: 'dot' }
|
|
52
|
+
expect(patternRegistry.getPatternInnerContent(style)).toBe('<path d="custom"/>')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('returns svgContent from registry for a named fillPattern', () => {
|
|
56
|
+
const style = { fillPattern: 'dot' }
|
|
57
|
+
patternRegistry.register('dot', '<path d="M4 4" fill="{{foregroundColor}}"/>')
|
|
58
|
+
expect(patternRegistry.getPatternInnerContent(style)).toBe('<path d="M4 4" fill="{{foregroundColor}}"/>')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('returns null for an unregistered fillPattern name', () => {
|
|
62
|
+
const style = { fillPattern: 'unknown-pattern' }
|
|
63
|
+
expect(patternRegistry.getPatternInnerContent(style)).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('returns null when no pattern is configured', () => {
|
|
67
|
+
expect(patternRegistry.getPatternInnerContent({})).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('getKeyPatternPaths', () => {
|
|
72
|
+
test('returns border and content strings with colours injected', () => {
|
|
73
|
+
const dataset = {
|
|
74
|
+
fillPattern: 'dot',
|
|
75
|
+
fillPatternForegroundColor: 'red',
|
|
76
|
+
fillPatternBackgroundColor: 'white',
|
|
77
|
+
stroke: 'black'
|
|
78
|
+
}
|
|
79
|
+
const result = patternRegistry.getKeyPatternPaths(dataset, 'style-a')
|
|
80
|
+
expect(result).not.toBeNull()
|
|
81
|
+
expect(result.border).toContain('black') // stroke colour
|
|
82
|
+
expect(result.border).toContain('white') // background colour
|
|
83
|
+
expect(result.content).toContain('red') // foreground colour
|
|
84
|
+
expect(result.border).not.toContain('{{foregroundColor}}')
|
|
85
|
+
expect(result.content).not.toContain('{{foregroundColor}}')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('returns null when no pattern content is found', () => {
|
|
89
|
+
expect(patternRegistry.getKeyPatternPaths({ fillPattern: 'unknown' }, 'style-a')).toBeNull()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('falls back to "black" fg and "transparent" bg when colour properties are absent', () => {
|
|
93
|
+
const result = patternRegistry.getKeyPatternPaths({ fillPattern: 'dot' }, 'style-a')
|
|
94
|
+
expect(result).not.toBeNull()
|
|
95
|
+
expect(result.content).toContain('black')
|
|
96
|
+
expect(result.border).toContain('transparent')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('border stroke falls back to foreground colour when stroke is absent', () => {
|
|
100
|
+
const result = patternRegistry.getKeyPatternPaths(
|
|
101
|
+
{ fillPattern: 'dot', fillPatternForegroundColor: 'green' },
|
|
102
|
+
'style-a'
|
|
103
|
+
)
|
|
104
|
+
expect(result).not.toBeNull()
|
|
105
|
+
// borderStroke falls back to fg ('green'), so the border uses green for both stroke and background
|
|
106
|
+
expect(result.border).toContain('green')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('KEY_BORDER_PATH contains foregroundColor and backgroundColor tokens', () => {
|
|
110
|
+
expect(KEY_BORDER_PATH).toContain('{{foregroundColor}}')
|
|
111
|
+
expect(KEY_BORDER_PATH).toContain('{{backgroundColor}}')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('patternRegistry.getPatternImageId', () => {
|
|
116
|
+
test('returns a deterministic string id', () => {
|
|
117
|
+
const dataset = { fillPattern: 'dot', fillPatternForegroundColor: 'red', fillPatternBackgroundColor: 'blue' }
|
|
118
|
+
const id = patternRegistry.getPatternImageId(dataset, 'style-a')
|
|
119
|
+
expect(typeof id).toBe('string')
|
|
120
|
+
expect(id).toMatch(/^pattern-/)
|
|
121
|
+
expect(id).toBe(patternRegistry.getPatternImageId(dataset, 'style-a'))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('returns null when no pattern content is found', () => {
|
|
125
|
+
expect(patternRegistry.getPatternImageId({ fillPattern: 'unknown' }, 'style-a')).toBeNull()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('produces different ids for different colours', () => {
|
|
129
|
+
const base = { fillPattern: 'dot' }
|
|
130
|
+
const idA = patternRegistry.getPatternImageId({ ...base, fillPatternForegroundColor: 'red' }, 'style-a')
|
|
131
|
+
const idB = patternRegistry.getPatternImageId({ ...base, fillPatternForegroundColor: 'blue' }, 'style-a')
|
|
132
|
+
expect(idA).not.toBe(idB)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('floors effective ratio at 2 * so low-DPI ids match 2x', () => {
|
|
136
|
+
const dataset = { fillPattern: 'dot' }
|
|
137
|
+
const id1x = patternRegistry.getPatternImageId(dataset, 'style-a', 1)
|
|
138
|
+
expect(id1x).toMatch(/-2x$/)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('produces different ids for pixelRatios above the floor', () => {
|
|
142
|
+
const dataset = { fillPattern: 'dot' }
|
|
143
|
+
const id2x = patternRegistry.getPatternImageId(dataset, 'style-a', 2)
|
|
144
|
+
const id3x = patternRegistry.getPatternImageId(dataset, 'style-a', 3)
|
|
145
|
+
expect(id2x).not.toBe(id3x)
|
|
146
|
+
expect(id2x).toMatch(/-4x$/)
|
|
147
|
+
expect(id3x).toMatch(/-6x$/)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('falls back to "black" foreground and "transparent" background when colours are absent', () => {
|
|
151
|
+
const id = patternRegistry.getPatternImageId({ fillPattern: 'dot' }, 'style-a')
|
|
152
|
+
const idExplicit = patternRegistry.getPatternImageId(
|
|
153
|
+
{ fillPattern: 'dot', fillPatternForegroundColor: 'black', fillPatternBackgroundColor: 'transparent' },
|
|
154
|
+
'style-a'
|
|
155
|
+
)
|
|
156
|
+
expect(id).toBe(idExplicit)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
48
159
|
})
|
|
@@ -7,6 +7,10 @@ export function createReverseGeocode ({ url, transformRequest, load }, crs) {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export function hasReverseGeocode () {
|
|
11
|
+
return reverseGeocodeFn !== null
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
export function reverseGeocode (zoom, coord) {
|
|
11
15
|
if (!reverseGeocodeFn) {
|
|
12
16
|
throw new Error('ReverseGeocode not initialised')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// reverseGeocode.test.js
|
|
2
|
-
import { createReverseGeocode, reverseGeocode } from './reverseGeocode'
|
|
2
|
+
import { createReverseGeocode, reverseGeocode, hasReverseGeocode } from './reverseGeocode'
|
|
3
3
|
|
|
4
4
|
describe('reverseGeocode module', () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -11,6 +11,10 @@ describe('reverseGeocode module', () => {
|
|
|
11
11
|
expect(() => reverseGeocode(10, [0, 0])).toThrow('ReverseGeocode not initialised')
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
+
it('hasReverseGeocode returns false before initialisation', () => {
|
|
15
|
+
expect(hasReverseGeocode()).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
14
18
|
it('sets reverseGeocodeFn via createReverseGeocode and calls providerFn correctly', async () => {
|
|
15
19
|
const mockProviderFn = jest.fn().mockResolvedValue('result')
|
|
16
20
|
|
|
@@ -44,6 +48,12 @@ describe('reverseGeocode module', () => {
|
|
|
44
48
|
expect(result).toBe('result')
|
|
45
49
|
})
|
|
46
50
|
|
|
51
|
+
it('hasReverseGeocode returns true after initialisation', () => {
|
|
52
|
+
const config = { url: 'x', transformRequest: jest.fn(), load: jest.fn().mockResolvedValue(jest.fn()) }
|
|
53
|
+
createReverseGeocode(config, 'EPSG:4326')
|
|
54
|
+
expect(hasReverseGeocode()).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
47
57
|
it('supports multiple calls after initialisation', async () => {
|
|
48
58
|
const mockProviderFn = jest.fn().mockResolvedValue('ok')
|
|
49
59
|
|