@defra/interactive-map 0.0.17-alpha → 0.0.19-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/assets/css/docusaurus.css +58 -34
- 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/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/context.md +53 -7
- package/docs/api/map-style-config.md +41 -2
- package/docs/api/marker-config.md +53 -11
- package/docs/api/panel-definition.md +16 -0
- package/docs/api/symbol-config.md +160 -0
- package/docs/api/symbol-registry.md +115 -0
- package/docs/api.md +50 -23
- package/docs/assets/basic-map.jpg +0 -0
- package/docs/assets/button-first.jpg +0 -0
- package/docs/assets/maker-panel.jpg +0 -0
- package/docs/examples/add-marker-with-panel.mdx +59 -0
- package/docs/examples/basic-map.mdx +24 -0
- package/docs/examples/button-map.mdx +24 -0
- package/docs/examples/index.mdx +49 -0
- package/docs/index.mdx +1 -1
- package/docs/plugins/datasets.md +105 -9
- package/docs/plugins/interact.md +100 -44
- package/docs/plugins/search.md +15 -3
- package/docs/plugins.md +1 -1
- package/docusaurus.config.cjs +9 -1
- package/package.json +1 -1
- package/plugins/beta/datasets/dist/css/index.css +32 -14
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/esm/index.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/DatasetsInit.jsx +9 -4
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +57 -11
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +14 -8
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +155 -53
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
- package/plugins/beta/datasets/src/api/addDataset.js +1 -1
- package/plugins/beta/datasets/src/api/setData.js +4 -2
- package/plugins/beta/datasets/src/api/setStyle.js +2 -2
- package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
- package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
- package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
- package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
- package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
- package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
- package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
- package/plugins/beta/datasets/src/datasets.js +13 -4
- package/plugins/beta/datasets/src/defaults.js +4 -2
- package/plugins/beta/datasets/src/index.js +2 -1
- package/plugins/beta/datasets/src/manifest.js +1 -1
- package/plugins/beta/datasets/src/panels/Key.jsx +11 -89
- package/plugins/beta/datasets/src/panels/Key.module.scss +24 -13
- package/plugins/beta/datasets/src/panels/Layers.module.scss +13 -7
- package/plugins/beta/datasets/src/reducer.js +6 -0
- package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
- package/plugins/beta/datasets/src/utils/mergeSublayer.js +8 -0
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
- package/plugins/beta/draw-ml/dist/css/index.css +3 -0
- 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/draw-ml/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
- 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/map-styles/src/MapStyles.jsx +5 -4
- package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
- package/plugins/beta/scale-bar/dist/css/index.css +1 -1
- package/plugins/beta/scale-bar/src/scaleBar.scss +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 +19 -8
- package/plugins/interact/src/InteractInit.test.js +26 -6
- package/plugins/interact/src/api/clear.js +1 -1
- package/plugins/interact/src/api/enable.test.js +7 -7
- package/plugins/interact/src/api/selectMarker.js +14 -0
- package/plugins/interact/src/api/selectMarker.test.js +25 -0
- package/plugins/interact/src/api/unselectMarker.js +14 -0
- package/plugins/interact/src/api/unselectMarker.test.js +14 -0
- package/plugins/interact/src/defaults.js +4 -6
- package/plugins/interact/src/events.js +27 -36
- package/plugins/interact/src/events.test.js +119 -90
- package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
- package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
- package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
- package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
- package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
- package/plugins/interact/src/manifest.js +10 -2
- package/plugins/interact/src/reducer.js +59 -5
- package/plugins/interact/src/reducer.test.js +100 -12
- package/plugins/interact/src/utils/buildStylesMap.js +17 -4
- package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
- package/plugins/interact/src/utils/featureQueries.js +11 -6
- package/plugins/interact/src/utils/featureQueries.test.js +8 -1
- package/plugins/interact/src/utils/interactionModes.js +12 -0
- 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/Search.jsx +3 -1
- package/plugins/search/src/events/fetchSuggestions.js +6 -4
- package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
- package/plugins/search/src/events/formHandlers.js +3 -3
- package/plugins/search/src/events/formHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -2
- package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
- package/plugins/search/src/utils/updateMap.js +3 -3
- package/plugins/search/src/utils/updateMap.test.js +3 -3
- package/providers/maplibre/dist/esm/im-maplibre-provider.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/appEvents.js +7 -0
- package/providers/maplibre/src/appEvents.test.js +18 -4
- package/providers/maplibre/src/maplibreProvider.js +52 -0
- package/providers/maplibre/src/maplibreProvider.test.js +105 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +36 -7
- package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -96
- package/providers/maplibre/src/utils/hoverCursor.js +61 -0
- package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
- package/providers/maplibre/src/utils/patternImages.js +70 -0
- package/providers/maplibre/src/utils/patternImages.test.js +180 -0
- package/providers/maplibre/src/utils/queryFeatures.js +38 -16
- package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
- package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
- package/providers/maplibre/src/utils/symbolImages.js +147 -0
- package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
- package/src/App/components/Markers/Markers.jsx +122 -27
- package/src/App/components/Markers/Markers.module.scss +0 -10
- package/src/App/components/Markers/Markers.test.jsx +246 -0
- package/src/App/components/Panel/Panel.jsx +6 -6
- package/src/App/components/Panel/Panel.test.jsx +37 -0
- package/src/App/components/Viewport/Viewport.jsx +5 -15
- package/src/App/components/Viewport/Viewport.module.scss +2 -0
- package/src/App/components/Viewport/Viewport.test.jsx +16 -33
- package/src/App/hooks/useInterfaceAPI.js +7 -7
- package/src/App/hooks/useInterfaceAPI.test.js +162 -0
- package/src/App/hooks/useLayoutMeasurements.js +64 -72
- package/src/App/hooks/useMarkersAPI.js +2 -5
- package/src/App/hooks/useMarkersAPI.test.js +4 -4
- package/src/App/layout/Layout.jsx +3 -3
- package/src/App/layout/Layout.test.jsx +4 -2
- package/src/App/layout/layout.module.scss +1 -8
- package/src/App/renderer/HtmlElementHost.jsx +10 -5
- package/src/App/renderer/mapPanels.js +2 -1
- package/src/App/store/ServiceProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +4 -4
- package/src/App/store/appActionsMap.test.js +10 -0
- package/src/App/store/mapActionsMap.js +4 -6
- package/src/App/store/mapActionsMap.test.js +3 -2
- package/src/App/store/mapReducer.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +59 -11
- package/src/InteractiveMap/InteractiveMap.test.js +126 -4
- package/src/InteractiveMap/domStateManager.js +18 -6
- package/src/InteractiveMap/domStateManager.test.js +21 -0
- package/src/InteractiveMap/historyManager.js +28 -16
- package/src/InteractiveMap/historyManager.test.js +17 -0
- package/src/config/appConfig.js +2 -7
- package/src/config/appConfig.test.js +4 -15
- package/src/config/defaults.js +2 -3
- package/src/config/events.js +20 -21
- package/src/config/mapTheme.js +56 -0
- package/src/config/patternConfig.js +16 -0
- package/src/config/symbolConfig.js +80 -0
- package/src/scss/settings/_colors.scss +0 -9
- package/src/services/closeApp.js +1 -10
- package/src/services/closeApp.test.js +3 -43
- package/src/services/patternRegistry.js +40 -0
- package/src/services/patternRegistry.test.js +48 -0
- package/src/services/symbolRegistry.js +113 -0
- package/src/services/symbolRegistry.test.js +262 -0
- package/src/types.js +99 -12
- package/src/utils/mapStateSync.js +48 -10
- package/src/utils/mapStateSync.test.js +29 -9
- package/src/utils/patternUtils.js +94 -0
- package/src/utils/patternUtils.test.js +160 -0
- package/src/utils/symbolUtils.js +85 -0
- package/src/utils/symbolUtils.test.js +156 -0
- package/docs/examples.mdx +0 -70
- package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +0 -48
- package/plugins/beta/datasets/src/styles/patterns.js +0 -157
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getValueForStyle } from '../utils/getValueForStyle.js'
|
|
2
|
+
import { symbolDefaults, pin, circle, square, graphics } from '../config/symbolConfig.js'
|
|
3
|
+
import { SCHEME_COLORS } from '../config/mapTheme.js'
|
|
4
|
+
|
|
5
|
+
const symbols = new Map()
|
|
6
|
+
let _constructorDefaults = {}
|
|
7
|
+
|
|
8
|
+
// Keys that are structural — not token values for SVG substitution
|
|
9
|
+
const STRUCTURAL = new Set(['id', 'svg', 'viewBox', 'anchor', 'symbol', 'symbolSvgContent'])
|
|
10
|
+
|
|
11
|
+
// selectedWidth is app-wide — not overridable at symbol registration level.
|
|
12
|
+
// selectedColor is a map style concern — always injected from mapStyle, never from cascade.
|
|
13
|
+
const REGISTRY_EXCLUDED = new Set([...STRUCTURAL, 'selectedWidth'])
|
|
14
|
+
|
|
15
|
+
function resolveValues (symbolDef, markerValues, mapStyle) {
|
|
16
|
+
const mapStyleId = mapStyle?.id
|
|
17
|
+
const symbolTokens = Object.fromEntries(
|
|
18
|
+
Object.entries(symbolDef || {}).filter(([k]) => !REGISTRY_EXCLUDED.has(k))
|
|
19
|
+
)
|
|
20
|
+
const constructorTokens = Object.fromEntries(
|
|
21
|
+
Object.entries(_constructorDefaults).filter(([k]) => !STRUCTURAL.has(k))
|
|
22
|
+
)
|
|
23
|
+
const defined = Object.fromEntries(
|
|
24
|
+
Object.entries(markerValues).filter(([, v]) => v != null)
|
|
25
|
+
)
|
|
26
|
+
const merged = { ...symbolDefaults, ...constructorTokens, ...symbolTokens, ...defined }
|
|
27
|
+
// haloColor and selectedColor are map style concerns — always injected from mapStyle, never from the cascade
|
|
28
|
+
const scheme = SCHEME_COLORS[mapStyle?.mapColorScheme] ?? SCHEME_COLORS.light
|
|
29
|
+
merged.haloColor = mapStyle?.haloColor ?? scheme.haloColor
|
|
30
|
+
merged.selectedColor = mapStyle?.selectedColor ?? scheme.selectedColor
|
|
31
|
+
if (typeof merged.graphic === 'string' && graphics[merged.graphic]) {
|
|
32
|
+
merged.graphic = graphics[merged.graphic]
|
|
33
|
+
}
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
Object.entries(merged).map(([token, value]) => [token, getValueForStyle(value, mapStyleId) || ''])
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveLayer (svgString, values) {
|
|
40
|
+
return Object.entries(values).reduce(
|
|
41
|
+
(svg, [token, value]) => svg.replaceAll(`{{${token}}}`, value),
|
|
42
|
+
svgString
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const symbolRegistry = {
|
|
47
|
+
/**
|
|
48
|
+
* Set constructor-level defaults. Called once during app initialisation.
|
|
49
|
+
* Merges onto symbolDefaults to form the app-wide token baseline.
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} defaults - Constructor symbolDefaults config
|
|
52
|
+
*/
|
|
53
|
+
setDefaults (defaults) {
|
|
54
|
+
_constructorDefaults = defaults || {}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns the merged app-wide defaults (hardcoded + constructor overrides).
|
|
59
|
+
* Includes both structural properties (symbol, viewBox, anchor) and token values.
|
|
60
|
+
*
|
|
61
|
+
* @returns {Object}
|
|
62
|
+
*/
|
|
63
|
+
getDefaults () {
|
|
64
|
+
return { ...symbolDefaults, ..._constructorDefaults }
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
register (symbolDef) {
|
|
68
|
+
symbols.set(symbolDef.id, symbolDef)
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
get (id) {
|
|
72
|
+
return symbols.get(id)
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
list () {
|
|
76
|
+
return [...symbols.values()]
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a symbol's SVG string for normal (unselected) rendering.
|
|
81
|
+
* The selected ring is always hidden regardless of cascade values.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} symbolDef - Symbol definition
|
|
84
|
+
* @param {Object} styleColors - Token overrides
|
|
85
|
+
* @param {Object} mapStyle - Current map style config (provides selectedColor, haloColor)
|
|
86
|
+
* @returns {string} Resolved SVG string
|
|
87
|
+
*/
|
|
88
|
+
resolve (symbolDef, styleColors, mapStyle) {
|
|
89
|
+
const colors = resolveValues(symbolDef, styleColors || {}, mapStyle)
|
|
90
|
+
if (!symbolDef) { return '' }
|
|
91
|
+
colors.selectedColor = ''
|
|
92
|
+
return resolveLayer(symbolDef.svg, colors)
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve a symbol's SVG string for selected rendering.
|
|
97
|
+
* selectedColor comes from mapStyle.selectedColor (or the hardcoded fallback).
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} symbolDef - Symbol definition
|
|
100
|
+
* @param {Object} styleColors - Token overrides
|
|
101
|
+
* @param {Object} mapStyle - Current map style config (provides selectedColor, haloColor)
|
|
102
|
+
* @returns {string} Resolved SVG string
|
|
103
|
+
*/
|
|
104
|
+
resolveSelected (symbolDef, styleColors, mapStyle) {
|
|
105
|
+
const colors = resolveValues(symbolDef, styleColors || {}, mapStyle)
|
|
106
|
+
if (!symbolDef) { return '' }
|
|
107
|
+
return resolveLayer(symbolDef.svg, colors)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
symbolRegistry.register(pin)
|
|
112
|
+
symbolRegistry.register(circle)
|
|
113
|
+
symbolRegistry.register(square)
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { symbolRegistry } from './symbolRegistry.js'
|
|
2
|
+
import { symbolDefaults } from '../config/symbolConfig.js'
|
|
3
|
+
import { SCHEME_COLORS } from '../config/mapTheme.js'
|
|
4
|
+
import { getValueForStyle } from '../utils/getValueForStyle.js'
|
|
5
|
+
|
|
6
|
+
const STYLE_ID = 'test'
|
|
7
|
+
const mapStyle = { id: STYLE_ID }
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
symbolRegistry.setDefaults({})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('symbolRegistry — built-in symbols', () => {
|
|
14
|
+
it('registers pin by default', () => {
|
|
15
|
+
const pin = symbolRegistry.get('pin')
|
|
16
|
+
expect(pin).toBeDefined()
|
|
17
|
+
expect(pin.id).toBe('pin')
|
|
18
|
+
expect(pin.anchor).toEqual([0.5, 0.9])
|
|
19
|
+
expect(typeof pin.svg).toBe('string')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('registers circle by default', () => {
|
|
23
|
+
const circle = symbolRegistry.get('circle')
|
|
24
|
+
expect(circle).toBeDefined()
|
|
25
|
+
expect(circle.id).toBe('circle')
|
|
26
|
+
expect(circle.anchor).toEqual([0.5, 0.5])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('lists both built-in symbols', () => {
|
|
30
|
+
const ids = symbolRegistry.list().map(s => s.id)
|
|
31
|
+
expect(ids).toContain('pin')
|
|
32
|
+
expect(ids).toContain('circle')
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('symbolRegistry — register / get', () => {
|
|
37
|
+
it('registers and retrieves a custom symbol', () => {
|
|
38
|
+
const custom = {
|
|
39
|
+
id: 'test-diamond',
|
|
40
|
+
viewBox: '0 0 20 20',
|
|
41
|
+
anchor: [0.5, 0.5],
|
|
42
|
+
svg: '<rect fill="{{backgroundColor}}"/>'
|
|
43
|
+
}
|
|
44
|
+
symbolRegistry.register(custom)
|
|
45
|
+
expect(symbolRegistry.get('test-diamond')).toBe(custom)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns undefined for an unregistered id', () => {
|
|
49
|
+
expect(symbolRegistry.get('does-not-exist')).toBeUndefined()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('symbolRegistry — setDefaults / getDefaults', () => {
|
|
54
|
+
it('getDefaults returns hardcoded defaults when no constructor defaults set', () => {
|
|
55
|
+
const defaults = symbolRegistry.getDefaults()
|
|
56
|
+
expect(defaults.symbol).toBe('pin')
|
|
57
|
+
expect(defaults.backgroundColor).toBe(symbolDefaults.backgroundColor)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('constructor defaults override hardcoded defaults', () => {
|
|
61
|
+
symbolRegistry.setDefaults({ backgroundColor: '#ff0000', symbol: 'circle' })
|
|
62
|
+
const defaults = symbolRegistry.getDefaults()
|
|
63
|
+
expect(defaults.backgroundColor).toBe('#ff0000')
|
|
64
|
+
expect(defaults.symbol).toBe('circle')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('constructor defaults do not affect unset properties', () => {
|
|
68
|
+
symbolRegistry.setDefaults({ backgroundColor: '#ff0000' })
|
|
69
|
+
const defaults = symbolRegistry.getDefaults()
|
|
70
|
+
expect(defaults.foregroundColor).toBe(symbolDefaults.foregroundColor)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('setDefaults with null or undefined resets to hardcoded defaults', () => {
|
|
74
|
+
symbolRegistry.setDefaults({ backgroundColor: '#ff0000' })
|
|
75
|
+
symbolRegistry.setDefaults(null)
|
|
76
|
+
expect(symbolRegistry.getDefaults().backgroundColor).toBe(symbolDefaults.backgroundColor)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('symbolRegistry — resolve', () => {
|
|
81
|
+
const BACKGROUND_SVG = '<path fill="{{backgroundColor}}"/>'
|
|
82
|
+
const symbolDef = {
|
|
83
|
+
id: 'test',
|
|
84
|
+
svg: '<path fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/><path fill="{{foregroundColor}}" stroke="{{selectedColor}}"/>'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
it('injects default token values when no overrides given', () => {
|
|
88
|
+
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
89
|
+
expect(resolved).toContain(`fill="${getValueForStyle(symbolDefaults.backgroundColor, STYLE_ID)}"`)
|
|
90
|
+
expect(resolved).toContain(`fill="${getValueForStyle(symbolDefaults.foregroundColor, STYLE_ID)}"`)
|
|
91
|
+
expect(resolved).toContain(`stroke-width="${symbolDefaults.haloWidth}"`)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('always produces empty selectedColor token — ring is hidden', () => {
|
|
95
|
+
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
96
|
+
expect(resolved).toContain('stroke=""')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('uses light scheme haloColor when mapStyle has no haloColor', () => {
|
|
100
|
+
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
101
|
+
expect(resolved).toContain(`stroke="${SCHEME_COLORS.light.haloColor}"`)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('uses mapStyle.haloColor when provided', () => {
|
|
105
|
+
const resolved = symbolRegistry.resolve(symbolDef, {}, { id: STYLE_ID, haloColor: '#336699' })
|
|
106
|
+
expect(resolved).toContain('stroke="#336699"')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('overrides default backgroundColor with a plain string', () => {
|
|
110
|
+
const resolved = symbolRegistry.resolve(symbolDef, { backgroundColor: '#ff0000' }, mapStyle)
|
|
111
|
+
expect(resolved).toContain('fill="#ff0000"')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('overrides default with a style-keyed color', () => {
|
|
115
|
+
const resolved = symbolRegistry.resolve(symbolDef, { backgroundColor: { [STYLE_ID]: '#aabbcc', other: '#112233' } }, mapStyle)
|
|
116
|
+
expect(resolved).toContain('fill="#aabbcc"')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('ignores null override values — defaults are preserved', () => {
|
|
120
|
+
const resolved = symbolRegistry.resolve(symbolDef, { backgroundColor: null }, mapStyle)
|
|
121
|
+
expect(resolved).toContain(`fill="${getValueForStyle(symbolDefaults.backgroundColor, STYLE_ID)}"`)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('replaces custom tokens not in defaults', () => {
|
|
125
|
+
const customDef = { id: 'custom', svg: '<path fill="{{accentColor}}"/>' }
|
|
126
|
+
const resolved = symbolRegistry.resolve(customDef, { accentColor: '#123456' }, mapStyle)
|
|
127
|
+
expect(resolved).toContain('fill="#123456"')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('handles null styleColors — uses all defaults', () => {
|
|
131
|
+
const resolved = symbolRegistry.resolve(symbolDef, null, mapStyle)
|
|
132
|
+
expect(resolved).toContain(`fill="${getValueForStyle(symbolDefaults.backgroundColor, STYLE_ID)}"`)
|
|
133
|
+
expect(resolved).toContain(`fill="${getValueForStyle(symbolDefaults.foregroundColor, STYLE_ID)}"`)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('replaces token with empty string when override is an empty string', () => {
|
|
137
|
+
const def = { id: 'es', svg: BACKGROUND_SVG }
|
|
138
|
+
const resolved = symbolRegistry.resolve(def, { backgroundColor: '' }, mapStyle)
|
|
139
|
+
expect(resolved).toContain('fill=""')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('returns empty string for null symbolDef', () => {
|
|
143
|
+
expect(symbolRegistry.resolve(null, {}, mapStyle)).toBe('')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('constructor defaults take precedence over hardcoded defaults', () => {
|
|
147
|
+
symbolRegistry.setDefaults({ backgroundColor: '#abcdef' })
|
|
148
|
+
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
149
|
+
expect(resolved).toContain('fill="#abcdef"')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('symbol-level token defaults take precedence over constructor defaults', () => {
|
|
153
|
+
symbolRegistry.setDefaults({ backgroundColor: '#abcdef' })
|
|
154
|
+
const defWithToken = { id: 'td', svg: BACKGROUND_SVG, backgroundColor: '#111111' }
|
|
155
|
+
const resolved = symbolRegistry.resolve(defWithToken, {}, mapStyle)
|
|
156
|
+
expect(resolved).toContain('fill="#111111"')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('marker-level overrides take precedence over symbol-level defaults', () => {
|
|
160
|
+
const defWithToken = { id: 'td2', svg: BACKGROUND_SVG, backgroundColor: '#111111' }
|
|
161
|
+
const resolved = symbolRegistry.resolve(defWithToken, { backgroundColor: '#ffffff' }, mapStyle)
|
|
162
|
+
expect(resolved).toContain('fill="#ffffff"')
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('symbolRegistry — resolveSelected', () => {
|
|
167
|
+
const symbolDef = {
|
|
168
|
+
id: 'test-sel',
|
|
169
|
+
svg: '<path stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/><path fill="{{backgroundColor}}"/>'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
it('uses light scheme selectedColor when mapStyle has no selectedColor', () => {
|
|
173
|
+
const resolved = symbolRegistry.resolveSelected(symbolDef, {}, mapStyle)
|
|
174
|
+
expect(resolved).toContain(`stroke="${SCHEME_COLORS.light.selectedColor}"`)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('uses mapStyle.selectedColor when provided', () => {
|
|
178
|
+
const resolved = symbolRegistry.resolveSelected(symbolDef, {}, { id: STYLE_ID, selectedColor: '#ff0000' })
|
|
179
|
+
expect(resolved).toContain('stroke="#ff0000"')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('uses selectedWidth from symbolDefaults', () => {
|
|
183
|
+
const resolved = symbolRegistry.resolveSelected(symbolDef, {}, mapStyle)
|
|
184
|
+
expect(resolved).toContain(`stroke-width="${symbolDefaults.selectedWidth}"`)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('handles null styleColors — uses cascade defaults', () => {
|
|
188
|
+
const resolved = symbolRegistry.resolveSelected(symbolDef, null, mapStyle)
|
|
189
|
+
expect(resolved).toContain(`stroke="${SCHEME_COLORS.light.selectedColor}"`)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('returns empty string for null symbolDef', () => {
|
|
193
|
+
expect(symbolRegistry.resolveSelected(null, {}, mapStyle)).toBe('')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('symbol-level selectedColor is ignored — mapStyle wins', () => {
|
|
197
|
+
const defWithSelected = { ...symbolDef, selectedColor: '#00ff00' }
|
|
198
|
+
const resolved = symbolRegistry.resolveSelected(defWithSelected, {}, { id: STYLE_ID, selectedColor: '#ff0000' })
|
|
199
|
+
expect(resolved).toContain('stroke="#ff0000"')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('still resolves other tokens correctly', () => {
|
|
203
|
+
const resolved = symbolRegistry.resolveSelected(symbolDef, { backgroundColor: '#d4351c' }, mapStyle)
|
|
204
|
+
expect(resolved).toContain('fill="#d4351c"')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('symbolRegistry — graphic token', () => {
|
|
209
|
+
const graphicDef = {
|
|
210
|
+
id: 'test-graphic',
|
|
211
|
+
graphic: 'M10 10 L20 20',
|
|
212
|
+
svg: '<path d="{{graphic}}" fill="{{foregroundColor}}"/>'
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
it('substitutes graphic d attribute from symbol-level default', () => {
|
|
216
|
+
const resolved = symbolRegistry.resolve(graphicDef, {}, mapStyle)
|
|
217
|
+
expect(resolved).toContain('d="M10 10 L20 20"')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('resolves named graphic string to built-in path data', () => {
|
|
221
|
+
const resolved = symbolRegistry.resolve(graphicDef, { graphic: 'cross' }, mapStyle)
|
|
222
|
+
expect(resolved).toContain('d="M6 3H10V6H13V10H10V13H6V10H3V6H6Z"')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('overrides symbol-level graphic with marker-level value', () => {
|
|
226
|
+
const resolved = symbolRegistry.resolve(graphicDef, { graphic: 'M0 0 L38 38' }, mapStyle)
|
|
227
|
+
expect(resolved).toContain('d="M0 0 L38 38"')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('overrides graphic via constructor defaults', () => {
|
|
231
|
+
symbolRegistry.setDefaults({ graphic: 'M5 5 L15 15' })
|
|
232
|
+
const defNoGraphic = { id: 'no-graphic', svg: '<path d="{{graphic}}"/>' }
|
|
233
|
+
const resolved = symbolRegistry.resolve(defNoGraphic, {}, mapStyle)
|
|
234
|
+
expect(resolved).toContain('d="M5 5 L15 15"')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('marker-level graphic overrides constructor default', () => {
|
|
238
|
+
symbolRegistry.setDefaults({ graphic: 'M5 5 L15 15' })
|
|
239
|
+
const defNoGraphic = { id: 'no-graphic2', svg: '<path d="{{graphic}}"/>' }
|
|
240
|
+
const resolved = symbolRegistry.resolve(defNoGraphic, { graphic: 'M1 1 L2 2' }, mapStyle)
|
|
241
|
+
expect(resolved).toContain('d="M1 1 L2 2"')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('built-in pin symbol has a graphic default', () => {
|
|
245
|
+
const pin = symbolRegistry.get('pin')
|
|
246
|
+
expect(typeof pin.graphic).toBe('string')
|
|
247
|
+
expect(pin.graphic.length).toBeGreaterThan(0)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('built-in circle symbol has a graphic default', () => {
|
|
251
|
+
const circle = symbolRegistry.get('circle')
|
|
252
|
+
expect(typeof circle.graphic).toBe('string')
|
|
253
|
+
expect(circle.graphic.length).toBeGreaterThan(0)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('pin resolves graphic token into its svg within a g transform', () => {
|
|
257
|
+
const pin = symbolRegistry.get('pin')
|
|
258
|
+
const resolved = symbolRegistry.resolve(pin, {}, mapStyle)
|
|
259
|
+
expect(resolved).toContain(`d="${pin.graphic}"`)
|
|
260
|
+
expect(resolved).toContain('translate(19, 16) scale(0.8) translate(-8, -8)')
|
|
261
|
+
})
|
|
262
|
+
})
|
package/src/types.js
CHANGED
|
@@ -316,15 +316,67 @@
|
|
|
316
316
|
* Alt text for logo.
|
|
317
317
|
*
|
|
318
318
|
* @property {'light' | 'dark'} [mapColorScheme]
|
|
319
|
-
* Map
|
|
319
|
+
* Map colour scheme. Sets the default values of `haloColor`, `selectedColor`, and `foregroundColor`
|
|
320
|
+
* when not explicitly provided, and signals to map overlay components which tonal range to use.
|
|
321
|
+
* `'light'` (default): dark overlays on a light basemap. `'dark'`: light overlays on a dark or aerial basemap.
|
|
320
322
|
*
|
|
321
323
|
* @property {string} [thumbnail]
|
|
322
324
|
* URL to thumbnail image.
|
|
323
325
|
*
|
|
326
|
+
* @property {string} [haloColor]
|
|
327
|
+
* Halo colour for elements rendered on top of the map (e.g. symbol outlines). Provides contrast
|
|
328
|
+
* between overlay elements and the map background. Falls back to `#ffffff` (light) or `#0b0c0c` (dark).
|
|
329
|
+
* Injected as the `--map-overlay-halo-color` CSS custom property.
|
|
330
|
+
*
|
|
331
|
+
* @property {string} [selectedColor]
|
|
332
|
+
* Theme colour for selected state — used by map overlay components to indicate a selected feature.
|
|
333
|
+
* Falls back to `#0b0c0c` (light) or `#ffffff` (dark).
|
|
334
|
+
* Injected as the `--map-overlay-selected-color` CSS custom property.
|
|
335
|
+
*
|
|
336
|
+
* @property {string} [foregroundColor]
|
|
337
|
+
* Foreground colour for elements rendered on top of the map (e.g. text or iconography in overlay components).
|
|
338
|
+
* Falls back to `#0b0c0c` (light) or `#ffffff` (dark).
|
|
339
|
+
* Injected as the `--map-overlay-foreground-color` CSS custom property.
|
|
340
|
+
*
|
|
324
341
|
* @property {string} url
|
|
325
342
|
* URL to the style.json (Mapbox Style Specification).
|
|
326
343
|
*/
|
|
327
344
|
|
|
345
|
+
/**
|
|
346
|
+
* App-wide symbol appearance defaults. All properties are optional.
|
|
347
|
+
* Color values may be a plain string or an object keyed by map style ID.
|
|
348
|
+
*
|
|
349
|
+
* @typedef {Object} SymbolDefaults
|
|
350
|
+
*
|
|
351
|
+
* @property {string} [symbol='pin']
|
|
352
|
+
* Default symbol ID. Built-in values: `'pin'`, `'circle'`.
|
|
353
|
+
*
|
|
354
|
+
* @property {string} [symbolSvgContent]
|
|
355
|
+
* Default inner SVG path content. When set, overrides `symbol`.
|
|
356
|
+
*
|
|
357
|
+
* @property {string} [viewBox='0 0 38 38']
|
|
358
|
+
* Default SVG viewBox.
|
|
359
|
+
*
|
|
360
|
+
* @property {[number, number]} [anchor=[0.5, 0.5]]
|
|
361
|
+
* Default anchor point as a normalised [x, y] pair.
|
|
362
|
+
*
|
|
363
|
+
* @property {string | Record<string, string>} [backgroundColor='#ca3535']
|
|
364
|
+
* Default background fill colour.
|
|
365
|
+
*
|
|
366
|
+
* @property {string | Record<string, string>} [foregroundColor='#ffffff']
|
|
367
|
+
* Default foreground fill colour.
|
|
368
|
+
*
|
|
369
|
+
* @property {string} [haloWidth='1']
|
|
370
|
+
* Default halo stroke width in SVG units.
|
|
371
|
+
*
|
|
372
|
+
* @property {string} [graphic]
|
|
373
|
+
* Default SVG `d` attribute value for the foreground graphic path. Each built-in symbol sets
|
|
374
|
+
* its own default (a small dot). Override to swap the graphic across all markers globally.
|
|
375
|
+
*
|
|
376
|
+
* @property {string} [selectedWidth='6']
|
|
377
|
+
* Selection ring stroke width in SVG units. App-wide only — ignored at symbol registration and marker creation level.
|
|
378
|
+
*/
|
|
379
|
+
|
|
328
380
|
/**
|
|
329
381
|
* Configuration for a map marker.
|
|
330
382
|
*
|
|
@@ -341,15 +393,47 @@
|
|
|
341
393
|
*/
|
|
342
394
|
|
|
343
395
|
/**
|
|
344
|
-
* Options for customizing marker appearance.
|
|
396
|
+
* Options for customizing marker appearance. Any key corresponds to a token in the symbol's SVG template.
|
|
397
|
+
* Color values may be a plain string or an object keyed by map style ID e.g. `{ outdoor: '#fff', dark: '#000' }`.
|
|
345
398
|
*
|
|
346
399
|
* @typedef {Object} MarkerOptions
|
|
347
400
|
*
|
|
348
|
-
* @property {string
|
|
349
|
-
*
|
|
401
|
+
* @property {string} [symbol]
|
|
402
|
+
* Symbol id to use for this marker (e.g. 'pin', 'circle'). Overrides the default `symbolDefaults.symbol` option.
|
|
403
|
+
*
|
|
404
|
+
* @property {string} [symbolSvgContent]
|
|
405
|
+
* Inner SVG path content (no `<svg>` wrapper) to use instead of a registered symbol.
|
|
406
|
+
* Use `{{token}}` placeholders for colours — e.g. `fill="{{backgroundColor}}"`.
|
|
407
|
+
* When set, `symbol` is ignored.
|
|
408
|
+
*
|
|
409
|
+
* @property {string} [viewBox]
|
|
410
|
+
* SVG viewBox attribute for the symbol, e.g. `'0 0 38 38'`.
|
|
411
|
+
* Defaults to the registered symbol's viewBox, or `'0 0 38 38'`.
|
|
412
|
+
*
|
|
413
|
+
* @property {[number, number]} [anchor]
|
|
414
|
+
* Anchor point as a normalised [x, y] pair where [0, 0] is top-left and [1, 1] is bottom-right.
|
|
415
|
+
* Determines which point on the symbol aligns with the geographic coordinate.
|
|
416
|
+
* Defaults to the registered symbol's anchor, or `[0.5, 0.5]` (centre).
|
|
417
|
+
*
|
|
418
|
+
* @property {string | Record<string, string>} [backgroundColor]
|
|
419
|
+
* Background fill colour of the symbol.
|
|
420
|
+
*
|
|
421
|
+
* @property {string | Record<string, string>} [foregroundColor]
|
|
422
|
+
* Foreground fill colour of the symbol (e.g. the inner dot on a pin).
|
|
423
|
+
*
|
|
424
|
+
* @property {string} [haloWidth]
|
|
425
|
+
* Stroke width of the halo in SVG units. Defaults to `'1'`.
|
|
426
|
+
*
|
|
427
|
+
* @property {string} [graphic]
|
|
428
|
+
* SVG `d` attribute value for the foreground graphic path. Replaces the foreground shape of the
|
|
429
|
+
* symbol while keeping its background, halo and selection ring intact. Each built-in symbol
|
|
430
|
+
* (`pin`, `circle`) provides a default dot; pass a different `d` string to swap it.
|
|
431
|
+
* Use named values from `graphics.js` or supply your own path data.
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* import { graphics } from './config/symbolConfig.js'
|
|
435
|
+
* markers.add('id', coords, { symbol: 'pin', graphic: graphics.cross })
|
|
350
436
|
*
|
|
351
|
-
* @property {string} [shape]
|
|
352
|
-
* Marker shape (e.g., 'pin').
|
|
353
437
|
*/
|
|
354
438
|
|
|
355
439
|
/**
|
|
@@ -507,7 +591,7 @@
|
|
|
507
591
|
* @property {boolean} [enableFullscreen=false]
|
|
508
592
|
* Whether a toggle fullscreen button is displayed.
|
|
509
593
|
*
|
|
510
|
-
* @property {boolean} [enableZoomControls=
|
|
594
|
+
* @property {boolean} [enableZoomControls=true]
|
|
511
595
|
* Whether zoom control buttons are displayed.
|
|
512
596
|
*
|
|
513
597
|
* @property {[number, number, number, number]} [extent]
|
|
@@ -540,14 +624,12 @@
|
|
|
540
624
|
* @property {string} [mapViewParamKey='mv']
|
|
541
625
|
* URL query parameter key used to control map view state.
|
|
542
626
|
*
|
|
543
|
-
* @property {string | Record<string, string>} [markerColor='#ff0000']
|
|
544
|
-
* Colour used for map markers. May be a single colour value or an object keyed by map style ID.
|
|
545
|
-
*
|
|
546
627
|
* @property {MarkerConfig[]} [markers]
|
|
547
628
|
* Initial markers to display on the map.
|
|
548
629
|
*
|
|
549
|
-
* @property {
|
|
550
|
-
*
|
|
630
|
+
* @property {Partial<SymbolDefaults>} [symbolDefaults]
|
|
631
|
+
* App-wide defaults for symbol appearance. Merged onto the hardcoded defaults in symbolDefaults.js.
|
|
632
|
+
* Values cascade: symbolDefaults.js → constructor symbolDefaults → symbol registration → marker creation.
|
|
551
633
|
*
|
|
552
634
|
* @property {[number, number, number, number]} [maxExtent]
|
|
553
635
|
* Maximum viewable extent [west, south, east, north].
|
|
@@ -579,6 +661,11 @@
|
|
|
579
661
|
* @property {PluginDescriptor[]} [plugins]
|
|
580
662
|
* Plugins to load.
|
|
581
663
|
*
|
|
664
|
+
* @property {boolean} [manageHistoryState=true]
|
|
665
|
+
* Whether the library should manage browser history state (pushState/replaceState) when opening and closing the map.
|
|
666
|
+
* Set to `false` in SPA frameworks (e.g. React Router, Docusaurus) that intercept history API calls.
|
|
667
|
+
* When `false`, listen to `APP_OPENED` and `APP_CLOSED` events and manage navigation in your router instead.
|
|
668
|
+
*
|
|
582
669
|
* @property {boolean} [preserveStateOnClose=false]
|
|
583
670
|
* Whether to preserve the map state when closed via back button or exit button.
|
|
584
671
|
* When true, the map is hidden but not destroyed, preserving markers, zoom, etc.
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Reads center and zoom for a given map ID from a URL search string.
|
|
3
|
+
* The `search` parameter is accepted explicitly to keep this function pure and testable.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} id - Map instance ID.
|
|
6
|
+
* @param {string} search - URL search string (e.g. `window.location.search`).
|
|
7
|
+
* @returns {{ center: [number, number], zoom: number } | null}
|
|
8
|
+
*/
|
|
2
9
|
const getMapStateFromURL = (id, search) => {
|
|
3
10
|
const params = new URLSearchParams(search)
|
|
4
11
|
const centerStr = params.get(`${id}:center`)
|
|
@@ -11,30 +18,61 @@ const getMapStateFromURL = (id, search) => {
|
|
|
11
18
|
return { center: [lng, lat], zoom }
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Persists map center and zoom into the page URL without triggering navigation.
|
|
23
|
+
*
|
|
24
|
+
* Existing query parameters are preserved. Parameters for this map ID are
|
|
25
|
+
* replaced if already present. Builds the URL manually to avoid percent-encoding
|
|
26
|
+
* colons and commas that URLSearchParams would otherwise encode.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} id - Map instance ID, used as a namespace prefix for the params.
|
|
29
|
+
* @param {{ center?: [number, number], zoom?: number }} state - Map state to write.
|
|
30
|
+
* @param {string} [currentHref] - URL to update. Defaults to `window.location.href`.
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
14
33
|
const setMapStateInURL = (id, state, currentHref = window.location.href) => {
|
|
15
|
-
// Use the passed href or the global one
|
|
16
34
|
const url = new URL(currentHref || 'http://localhost')
|
|
17
|
-
|
|
35
|
+
|
|
36
|
+
const newKeys = new Set()
|
|
18
37
|
const newParams = []
|
|
19
38
|
|
|
20
39
|
if (state.center) {
|
|
21
|
-
|
|
40
|
+
const key = `${id}:center`
|
|
41
|
+
newKeys.add(key)
|
|
42
|
+
newParams.push(`${key}=${state.center[0]},${state.center[1]}`)
|
|
22
43
|
}
|
|
23
44
|
if (state.zoom != null) {
|
|
24
|
-
|
|
45
|
+
const key = `${id}:zoom`
|
|
46
|
+
newKeys.add(key)
|
|
47
|
+
newParams.push(`${key}=${state.zoom}`)
|
|
25
48
|
}
|
|
26
49
|
|
|
27
|
-
const
|
|
28
|
-
|
|
50
|
+
const existingParams = []
|
|
51
|
+
url.searchParams.forEach((value, key) => {
|
|
52
|
+
if (!newKeys.has(key)) {
|
|
53
|
+
existingParams.push(`${key}=${value}`)
|
|
54
|
+
}
|
|
29
55
|
})
|
|
30
56
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
57
|
+
const allParams = [...existingParams, ...newParams].join('&')
|
|
58
|
+
const search = allParams ? '?' + allParams : ''
|
|
59
|
+
const newUrl = `${url.origin}${url.pathname}${search}${url.hash}`
|
|
33
60
|
window.history.replaceState(window.history.state, '', newUrl)
|
|
34
61
|
}
|
|
35
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Returns the initial map view state, preferring any saved state from the URL.
|
|
65
|
+
*
|
|
66
|
+
* Resolution order:
|
|
67
|
+
* 1. Center/zoom encoded in the URL search string for this map ID.
|
|
68
|
+
* 2. A `bounds` value if provided (used to fit the view on load).
|
|
69
|
+
* 3. The configured `center` and `zoom` defaults.
|
|
70
|
+
*
|
|
71
|
+
* @param {{ id: string, center: [number, number], zoom: number, bounds?: any }} config - Map config.
|
|
72
|
+
* @param {string} [search] - URL search string. Defaults to `window.location.search`.
|
|
73
|
+
* @returns {{ center: [number, number], zoom: number } | { bounds: any }}
|
|
74
|
+
*/
|
|
36
75
|
const getInitialMapState = ({ id, center, zoom, bounds }, search = window.location.search) => {
|
|
37
|
-
// Pass search string down to the internal function
|
|
38
76
|
const savedState = getMapStateFromURL(id, search)
|
|
39
77
|
if (savedState) {
|
|
40
78
|
return {
|
|
@@ -33,11 +33,10 @@ describe('mapStateSync utilities', () => {
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
describe('setMapStateInURL', () => {
|
|
36
|
-
it('
|
|
36
|
+
it('writes center and zoom into the URL', () => {
|
|
37
37
|
const mockHref = 'http://test.com/path?existing=true'
|
|
38
38
|
setMapStateInURL('map1', { center: [10, 20], zoom: 5 }, mockHref)
|
|
39
39
|
|
|
40
|
-
// Verification of Line 28: First arg must be global history state
|
|
41
40
|
expect(globalThis.history.replaceState).toHaveBeenCalledWith(
|
|
42
41
|
globalThis.history.state,
|
|
43
42
|
'',
|
|
@@ -45,21 +44,42 @@ describe('mapStateSync utilities', () => {
|
|
|
45
44
|
)
|
|
46
45
|
})
|
|
47
46
|
|
|
48
|
-
it('
|
|
47
|
+
it('replaces existing map params when already present in the URL', () => {
|
|
48
|
+
setMapStateInURL('map1', { center: [10, 20], zoom: 5 }, 'http://test.com?map1:center=1,2&map1:zoom=3')
|
|
49
|
+
const url = globalThis.history.replaceState.mock.calls[0][2]
|
|
50
|
+
expect(url).toContain('map1:center=10,20')
|
|
51
|
+
expect(url).toContain('map1:zoom=5')
|
|
52
|
+
expect(url).not.toContain('map1:center=1,2')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('preserves unrelated existing params (e.g. mv)', () => {
|
|
56
|
+
setMapStateInURL('map1', { center: [10, 20], zoom: 5 }, 'http://test.com/path?mv=map1')
|
|
57
|
+
const url = globalThis.history.replaceState.mock.calls[0][2]
|
|
58
|
+
expect(url).toContain('mv=map1')
|
|
59
|
+
expect(url).toContain('map1:center=10,20')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('uses fallback localhost URL when href is null', () => {
|
|
49
63
|
setMapStateInURL('map1', { zoom: 10 }, null)
|
|
50
|
-
const
|
|
51
|
-
expect(
|
|
64
|
+
const url = globalThis.history.replaceState.mock.calls[0][2]
|
|
65
|
+
expect(url).toContain('http://localhost')
|
|
52
66
|
})
|
|
53
67
|
|
|
54
|
-
it('
|
|
68
|
+
it('omits zoom when zoom is null', () => {
|
|
55
69
|
setMapStateInURL('map1', { center: [0, 0], zoom: null }, 'http://test.com')
|
|
56
|
-
const
|
|
57
|
-
expect(
|
|
70
|
+
const url = globalThis.history.replaceState.mock.calls[0][2]
|
|
71
|
+
expect(url).not.toContain('zoom')
|
|
58
72
|
})
|
|
59
73
|
|
|
60
|
-
it('
|
|
74
|
+
it('uses window.location.href when no href is provided', () => {
|
|
61
75
|
setMapStateInURL('map1', { zoom: 10 })
|
|
62
76
|
expect(globalThis.history.replaceState).toHaveBeenCalled()
|
|
63
77
|
})
|
|
78
|
+
|
|
79
|
+
it('produces a URL with no search string when state is empty and no existing params', () => {
|
|
80
|
+
setMapStateInURL('map1', {}, 'http://test.com/path')
|
|
81
|
+
const url = globalThis.history.replaceState.mock.calls[0][2]
|
|
82
|
+
expect(url).toBe('http://test.com/path')
|
|
83
|
+
})
|
|
64
84
|
})
|
|
65
85
|
})
|