@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,94 @@
|
|
|
1
|
+
import { getValueForStyle } from '../utils/getValueForStyle.js'
|
|
2
|
+
|
|
3
|
+
// Border path rendered behind the pattern content in Key panel symbols (20×20 coordinate space).
|
|
4
|
+
export const KEY_BORDER_PATH = '<path d="M19 2.862v14.275c0 1.028-.835 1.862-1.862 1.862H2.863c-1.028 0-1.862-.835-1.862-1.862V2.862C1.001 1.834 1.836 1 2.863 1h14.275C18.166 1 19 1.835 19 2.862z" fill="{{backgroundColor}}" stroke="{{foregroundColor}}" stroke-width="2"/>'
|
|
5
|
+
|
|
6
|
+
export const hashString = (str) => {
|
|
7
|
+
let hash = 0
|
|
8
|
+
for (const ch of str) {
|
|
9
|
+
hash = ((hash << 5) - hash) + ch.codePointAt(0)
|
|
10
|
+
hash = hash & hash
|
|
11
|
+
}
|
|
12
|
+
return Math.abs(hash).toString(36) // NOSONAR: base36 encoding for compact alphanumeric hash string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Replaces {{foregroundColor}} and {{backgroundColor}} tokens in SVG content with resolved colour values.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} content - SVG path string with colour tokens
|
|
19
|
+
* @param {string} foregroundColor
|
|
20
|
+
* @param {string} backgroundColor
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export const injectColors = (content, foregroundColor, backgroundColor) =>
|
|
24
|
+
content
|
|
25
|
+
.replace(/\{\{foregroundColor\}\}/g, foregroundColor || 'black')
|
|
26
|
+
.replace(/\{\{backgroundColor\}\}/g, backgroundColor || 'transparent')
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns true if a dataset/config has a fill pattern configured.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} dataset
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export const hasPattern = (dataset) => !!(dataset.fillPattern || dataset.fillPatternSvgContent)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the raw (un-coloured) inner SVG content for a dataset's pattern.
|
|
38
|
+
* Precedence: inline fillPatternSvgContent → named fillPattern from registry.
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} dataset
|
|
41
|
+
* @param {Object} patternRegistry
|
|
42
|
+
* @returns {string|null}
|
|
43
|
+
*/
|
|
44
|
+
export const getPatternInnerContent = (dataset, patternRegistry) => {
|
|
45
|
+
if (dataset.fillPatternSvgContent) {
|
|
46
|
+
return dataset.fillPatternSvgContent
|
|
47
|
+
}
|
|
48
|
+
if (dataset.fillPattern) {
|
|
49
|
+
return patternRegistry?.get(dataset.fillPattern)?.svgContent ?? null
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns a deterministic image ID for a pattern + resolved colour combination.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} dataset
|
|
58
|
+
* @param {string} mapStyleId
|
|
59
|
+
* @param {Object} patternRegistry
|
|
60
|
+
* @returns {string|null}
|
|
61
|
+
*/
|
|
62
|
+
export const getPatternImageId = (dataset, mapStyleId, patternRegistry) => {
|
|
63
|
+
const innerContent = getPatternInnerContent(dataset, patternRegistry)
|
|
64
|
+
if (!innerContent) {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
68
|
+
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
69
|
+
return `pattern-${hashString(innerContent + fg + bg)}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns colour-injected SVG path content for use in Key panel pattern symbols.
|
|
74
|
+
* Returns { border, content } where border is the rounded-rect outline and content
|
|
75
|
+
* is the pattern fill. Returns null if the dataset has no pattern.
|
|
76
|
+
*
|
|
77
|
+
* @param {Object} dataset
|
|
78
|
+
* @param {string} mapStyleId
|
|
79
|
+
* @param {Object} patternRegistry
|
|
80
|
+
* @returns {{ border: string, content: string }|null}
|
|
81
|
+
*/
|
|
82
|
+
export const getKeyPatternPaths = (dataset, mapStyleId, patternRegistry) => {
|
|
83
|
+
const innerContent = getPatternInnerContent(dataset, patternRegistry)
|
|
84
|
+
if (!innerContent) {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
88
|
+
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
89
|
+
const borderStroke = getValueForStyle(dataset.stroke, mapStyleId) || fg
|
|
90
|
+
return {
|
|
91
|
+
border: injectColors(KEY_BORDER_PATH, borderStroke, bg),
|
|
92
|
+
content: injectColors(innerContent, fg, bg)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hashString,
|
|
3
|
+
injectColors,
|
|
4
|
+
hasPattern,
|
|
5
|
+
getPatternInnerContent,
|
|
6
|
+
getPatternImageId,
|
|
7
|
+
getKeyPatternPaths,
|
|
8
|
+
KEY_BORDER_PATH
|
|
9
|
+
} from './patternUtils.js'
|
|
10
|
+
|
|
11
|
+
const mockRegistry = {
|
|
12
|
+
get: (id) => id === 'dot' ? { id: 'dot', svgContent: '<path d="M4 4" fill="{{foregroundColor}}"/>' } : undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('hashString', () => {
|
|
16
|
+
test('returns a non-empty string', () => {
|
|
17
|
+
expect(typeof hashString('hello')).toBe('string')
|
|
18
|
+
expect(hashString('hello').length).toBeGreaterThan(0)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('is deterministic', () => {
|
|
22
|
+
expect(hashString('hello')).toBe(hashString('hello'))
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('produces different values for different inputs', () => {
|
|
26
|
+
expect(hashString('a')).not.toBe(hashString('b'))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('injectColors', () => {
|
|
31
|
+
test('replaces {{foregroundColor}} and {{backgroundColor}} tokens', () => {
|
|
32
|
+
const result = injectColors('fill="{{foregroundColor}}" bg="{{backgroundColor}}"', 'red', 'blue')
|
|
33
|
+
expect(result).toBe('fill="red" bg="blue"')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('replaces all occurrences', () => {
|
|
37
|
+
const result = injectColors('{{foregroundColor}} {{foregroundColor}}', 'red', 'blue')
|
|
38
|
+
expect(result).toBe('red red')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('uses fallback "black" when foregroundColor is falsy', () => {
|
|
42
|
+
expect(injectColors('{{foregroundColor}}', '', 'blue')).toBe('black')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('uses fallback "transparent" when backgroundColor is falsy', () => {
|
|
46
|
+
expect(injectColors('{{backgroundColor}}', 'red', '')).toBe('transparent')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('hasPattern', () => {
|
|
51
|
+
test('returns true when fillPattern is set', () => {
|
|
52
|
+
expect(hasPattern({ fillPattern: 'dot' })).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('returns true when fillPatternSvgContent is set', () => {
|
|
56
|
+
expect(hasPattern({ fillPatternSvgContent: '<path/>' })).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('returns false when neither is set', () => {
|
|
60
|
+
expect(hasPattern({})).toBe(false)
|
|
61
|
+
expect(hasPattern({ fill: 'red' })).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('getPatternInnerContent', () => {
|
|
66
|
+
test('returns fillPatternSvgContent when set (inline SVG takes precedence)', () => {
|
|
67
|
+
const dataset = { fillPatternSvgContent: '<path d="custom"/>', fillPattern: 'dot' }
|
|
68
|
+
expect(getPatternInnerContent(dataset, mockRegistry)).toBe('<path d="custom"/>')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('returns svgContent from registry for a named fillPattern', () => {
|
|
72
|
+
const dataset = { fillPattern: 'dot' }
|
|
73
|
+
expect(getPatternInnerContent(dataset, mockRegistry)).toBe('<path d="M4 4" fill="{{foregroundColor}}"/>')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('returns null for an unregistered fillPattern name', () => {
|
|
77
|
+
const dataset = { fillPattern: 'unknown-pattern' }
|
|
78
|
+
expect(getPatternInnerContent(dataset, mockRegistry)).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('returns null when no pattern is configured', () => {
|
|
82
|
+
expect(getPatternInnerContent({}, mockRegistry)).toBeNull()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('getPatternImageId', () => {
|
|
87
|
+
test('returns a deterministic string id', () => {
|
|
88
|
+
const dataset = { fillPattern: 'dot', fillPatternForegroundColor: 'red', fillPatternBackgroundColor: 'blue' }
|
|
89
|
+
const id = getPatternImageId(dataset, 'style-a', mockRegistry)
|
|
90
|
+
expect(typeof id).toBe('string')
|
|
91
|
+
expect(id).toMatch(/^pattern-/)
|
|
92
|
+
expect(id).toBe(getPatternImageId(dataset, 'style-a', mockRegistry))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('returns null when no pattern content is found', () => {
|
|
96
|
+
expect(getPatternImageId({ fillPattern: 'unknown' }, 'style-a', mockRegistry)).toBeNull()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('produces different ids for different colours', () => {
|
|
100
|
+
const base = { fillPattern: 'dot' }
|
|
101
|
+
const idA = getPatternImageId({ ...base, fillPatternForegroundColor: 'red' }, 'style-a', mockRegistry)
|
|
102
|
+
const idB = getPatternImageId({ ...base, fillPatternForegroundColor: 'blue' }, 'style-a', mockRegistry)
|
|
103
|
+
expect(idA).not.toBe(idB)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('falls back to "black" foreground and "transparent" background when colours are absent', () => {
|
|
107
|
+
const id = getPatternImageId({ fillPattern: 'dot' }, 'style-a', mockRegistry)
|
|
108
|
+
const idExplicit = getPatternImageId(
|
|
109
|
+
{ fillPattern: 'dot', fillPatternForegroundColor: 'black', fillPatternBackgroundColor: 'transparent' },
|
|
110
|
+
'style-a',
|
|
111
|
+
mockRegistry
|
|
112
|
+
)
|
|
113
|
+
expect(id).toBe(idExplicit)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('getKeyPatternPaths', () => {
|
|
118
|
+
test('returns border and content strings with colours injected', () => {
|
|
119
|
+
const dataset = {
|
|
120
|
+
fillPattern: 'dot',
|
|
121
|
+
fillPatternForegroundColor: 'red',
|
|
122
|
+
fillPatternBackgroundColor: 'white',
|
|
123
|
+
stroke: 'black'
|
|
124
|
+
}
|
|
125
|
+
const result = getKeyPatternPaths(dataset, 'style-a', mockRegistry)
|
|
126
|
+
expect(result).not.toBeNull()
|
|
127
|
+
expect(result.border).toContain('black') // stroke colour
|
|
128
|
+
expect(result.border).toContain('white') // background colour
|
|
129
|
+
expect(result.content).toContain('red') // foreground colour
|
|
130
|
+
expect(result.border).not.toContain('{{foregroundColor}}')
|
|
131
|
+
expect(result.content).not.toContain('{{foregroundColor}}')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('returns null when no pattern content is found', () => {
|
|
135
|
+
expect(getKeyPatternPaths({ fillPattern: 'unknown' }, 'style-a', mockRegistry)).toBeNull()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('falls back to "black" fg and "transparent" bg when colour properties are absent', () => {
|
|
139
|
+
const result = getKeyPatternPaths({ fillPattern: 'dot' }, 'style-a', mockRegistry)
|
|
140
|
+
expect(result).not.toBeNull()
|
|
141
|
+
expect(result.content).toContain('black')
|
|
142
|
+
expect(result.border).toContain('transparent')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('border stroke falls back to foreground colour when stroke is absent', () => {
|
|
146
|
+
const result = getKeyPatternPaths(
|
|
147
|
+
{ fillPattern: 'dot', fillPatternForegroundColor: 'green' },
|
|
148
|
+
'style-a',
|
|
149
|
+
mockRegistry
|
|
150
|
+
)
|
|
151
|
+
expect(result).not.toBeNull()
|
|
152
|
+
// borderStroke falls back to fg ('green'), so the border uses green for both stroke and background
|
|
153
|
+
expect(result.border).toContain('green')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('KEY_BORDER_PATH contains foregroundColor and backgroundColor tokens', () => {
|
|
157
|
+
expect(KEY_BORDER_PATH).toContain('{{foregroundColor}}')
|
|
158
|
+
expect(KEY_BORDER_PATH).toContain('{{backgroundColor}}')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Symbol style props in dataset style that carry token values.
|
|
2
|
+
// These use the 'symbol' prefix to distinguish them from fill/stroke props at the same level.
|
|
3
|
+
// The prefix is stripped before passing tokens to the registry (e.g. symbolBackgroundColor → backgroundColor).
|
|
4
|
+
const SYMBOL_STYLE_PROPS = new Set([
|
|
5
|
+
'symbolBackgroundColor', 'symbolForegroundColor',
|
|
6
|
+
'symbolHaloWidth', 'symbolGraphic'
|
|
7
|
+
])
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if this dataset should be rendered as a symbol (point) layer.
|
|
11
|
+
* @param {Object} dataset
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export const hasSymbol = (dataset) => !!(dataset.symbol || dataset.symbolSvgContent)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the symbolDef for a dataset's symbol config.
|
|
18
|
+
*
|
|
19
|
+
* dataset.symbol is a string symbol ID (e.g. 'pin').
|
|
20
|
+
* dataset.symbolSvgContent is inline SVG content for a custom symbol.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} dataset
|
|
23
|
+
* @param {Object} symbolRegistry
|
|
24
|
+
* @returns {Object|undefined}
|
|
25
|
+
*/
|
|
26
|
+
export const getSymbolDef = (dataset, symbolRegistry) => {
|
|
27
|
+
if (dataset.symbolSvgContent) {
|
|
28
|
+
return { svg: dataset.symbolSvgContent }
|
|
29
|
+
}
|
|
30
|
+
if (dataset.symbol) {
|
|
31
|
+
return symbolRegistry.get(dataset.symbol)
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extracts token overrides from a dataset's flat symbol style props.
|
|
38
|
+
* Strips the 'symbol' prefix to produce internal token names (e.g. symbolBackgroundColor → backgroundColor).
|
|
39
|
+
* Returns an empty object when no symbol is configured.
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} dataset
|
|
42
|
+
* @returns {Object}
|
|
43
|
+
*/
|
|
44
|
+
export const getSymbolStyleColors = (dataset) => {
|
|
45
|
+
if (!hasSymbol(dataset)) { return {} }
|
|
46
|
+
const tokens = {}
|
|
47
|
+
SYMBOL_STYLE_PROPS.forEach(key => {
|
|
48
|
+
if (dataset[key] != null) {
|
|
49
|
+
// Strip 'symbol' prefix: symbolBackgroundColor → backgroundColor
|
|
50
|
+
const tokenKey = key.charAt(6).toLowerCase() + key.slice(7) // NOSONAR
|
|
51
|
+
tokens[tokenKey] = dataset[key]
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
return tokens
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns the viewBox string for a dataset's symbol.
|
|
59
|
+
* Precedence: dataset.symbolViewBox → symbolDef viewBox → default.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} dataset
|
|
62
|
+
* @param {Object|undefined} symbolDef
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export const getSymbolViewBox = (dataset, symbolDef) => {
|
|
66
|
+
if (dataset.symbolViewBox) {
|
|
67
|
+
return dataset.symbolViewBox
|
|
68
|
+
}
|
|
69
|
+
return symbolDef?.viewBox ?? '0 0 38 38'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the anchor for a dataset's symbol as [x, y] in 0–1 space.
|
|
74
|
+
* Precedence: dataset.symbolAnchor → symbolDef anchor → [0.5, 0.5].
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} dataset
|
|
77
|
+
* @param {Object|undefined} symbolDef
|
|
78
|
+
* @returns {number[]}
|
|
79
|
+
*/
|
|
80
|
+
export const getSymbolAnchor = (dataset, symbolDef) => {
|
|
81
|
+
if (dataset.symbolAnchor) {
|
|
82
|
+
return dataset.symbolAnchor
|
|
83
|
+
}
|
|
84
|
+
return symbolDef?.anchor ?? [0.5, 0.5]
|
|
85
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasSymbol,
|
|
3
|
+
getSymbolDef,
|
|
4
|
+
getSymbolStyleColors,
|
|
5
|
+
getSymbolViewBox,
|
|
6
|
+
getSymbolAnchor
|
|
7
|
+
} from './symbolUtils.js'
|
|
8
|
+
|
|
9
|
+
const mockRegistry = (defs = {}) => ({
|
|
10
|
+
get: jest.fn((id) => defs[id])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// ─── hasSymbol ────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
describe('hasSymbol', () => {
|
|
16
|
+
it('returns true when dataset has a symbol string', () => {
|
|
17
|
+
expect(hasSymbol({ symbol: 'pin' })).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns true when dataset has symbolSvgContent', () => {
|
|
21
|
+
expect(hasSymbol({ symbolSvgContent: '<circle/>' })).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('returns false when symbol is absent', () => {
|
|
25
|
+
expect(hasSymbol({})).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns false when symbol is null', () => {
|
|
29
|
+
expect(hasSymbol({ symbol: null })).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// ─── getSymbolDef ─────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe('getSymbolDef', () => {
|
|
36
|
+
it('returns undefined when dataset has no symbol', () => {
|
|
37
|
+
expect(getSymbolDef({}, mockRegistry())).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('looks up string symbol id in the registry', () => {
|
|
41
|
+
const pinDef = { id: 'pin', svg: '<g/>' }
|
|
42
|
+
const registry = mockRegistry({ pin: pinDef })
|
|
43
|
+
expect(getSymbolDef({ symbol: 'pin' }, registry)).toBe(pinDef)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns undefined for an unregistered string symbol', () => {
|
|
47
|
+
expect(getSymbolDef({ symbol: 'missing' }, mockRegistry())).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns inline def from symbolSvgContent with svg key', () => {
|
|
51
|
+
const dataset = { symbolSvgContent: '<circle/>', symbolViewBox: '0 0 10 10' }
|
|
52
|
+
const result = getSymbolDef(dataset, mockRegistry())
|
|
53
|
+
expect(result.svg).toBe('<circle/>')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('symbolSvgContent takes precedence over symbol id', () => {
|
|
57
|
+
const pinDef = { id: 'pin', svg: '<g/>' }
|
|
58
|
+
const registry = mockRegistry({ pin: pinDef })
|
|
59
|
+
const result = getSymbolDef({ symbol: 'pin', symbolSvgContent: '<circle/>' }, registry)
|
|
60
|
+
expect(result.svg).toBe('<circle/>')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ─── getSymbolStyleColors ─────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
describe('getSymbolStyleColors', () => {
|
|
67
|
+
it('returns empty object when dataset has no symbol', () => {
|
|
68
|
+
expect(getSymbolStyleColors({})).toEqual({})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns empty object for string symbol with no token props', () => {
|
|
72
|
+
expect(getSymbolStyleColors({ symbol: 'pin' })).toEqual({})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('strips symbol prefix from token props', () => {
|
|
76
|
+
const dataset = {
|
|
77
|
+
symbol: 'pin',
|
|
78
|
+
symbolBackgroundColor: '#ff0000',
|
|
79
|
+
symbolForegroundColor: '#ffffff',
|
|
80
|
+
symbolHaloWidth: '2',
|
|
81
|
+
symbolGraphic: 'cross'
|
|
82
|
+
}
|
|
83
|
+
expect(getSymbolStyleColors(dataset)).toEqual({
|
|
84
|
+
backgroundColor: '#ff0000',
|
|
85
|
+
foregroundColor: '#ffffff',
|
|
86
|
+
haloWidth: '2',
|
|
87
|
+
graphic: 'cross'
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('works with symbolSvgContent instead of symbol id', () => {
|
|
92
|
+
const dataset = { symbolSvgContent: '<circle/>', symbolBackgroundColor: '#0000ff' }
|
|
93
|
+
expect(getSymbolStyleColors(dataset)).toEqual({ backgroundColor: '#0000ff' })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('omits token props that are null or undefined', () => {
|
|
97
|
+
const dataset = { symbol: 'pin', symbolBackgroundColor: '#ff0000', symbolForegroundColor: null }
|
|
98
|
+
const result = getSymbolStyleColors(dataset)
|
|
99
|
+
expect(result).toEqual({ backgroundColor: '#ff0000' })
|
|
100
|
+
expect(result).not.toHaveProperty('foregroundColor')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('supports style-keyed colour objects', () => {
|
|
104
|
+
const dataset = {
|
|
105
|
+
symbol: 'pin',
|
|
106
|
+
symbolBackgroundColor: { outdoor: '#1d70b8', dark: '#5694ca' }
|
|
107
|
+
}
|
|
108
|
+
expect(getSymbolStyleColors(dataset)).toEqual({
|
|
109
|
+
backgroundColor: { outdoor: '#1d70b8', dark: '#5694ca' }
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// ─── getSymbolViewBox ─────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
describe('getSymbolViewBox', () => {
|
|
117
|
+
it('returns symbolViewBox from dataset', () => {
|
|
118
|
+
const dataset = { symbol: 'custom', symbolViewBox: '0 0 24 24' }
|
|
119
|
+
expect(getSymbolViewBox(dataset, undefined)).toBe('0 0 24 24')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('falls back to symbolDef viewBox', () => {
|
|
123
|
+
const symbolDef = { id: 'pin', viewBox: '0 0 38 38' }
|
|
124
|
+
expect(getSymbolViewBox({ symbol: 'pin' }, symbolDef)).toBe('0 0 38 38')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('returns default viewBox when neither source has one', () => {
|
|
128
|
+
expect(getSymbolViewBox({ symbol: 'pin' }, {})).toBe('0 0 38 38')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('returns default viewBox when symbolDef is undefined', () => {
|
|
132
|
+
expect(getSymbolViewBox({ symbol: 'pin' }, undefined)).toBe('0 0 38 38')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// ─── getSymbolAnchor ──────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
describe('getSymbolAnchor', () => {
|
|
139
|
+
it('returns symbolAnchor from dataset', () => {
|
|
140
|
+
const dataset = { symbol: 'custom', symbolAnchor: [0.5, 0.9] }
|
|
141
|
+
expect(getSymbolAnchor(dataset, undefined)).toEqual([0.5, 0.9])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('falls back to symbolDef anchor', () => {
|
|
145
|
+
const symbolDef = { id: 'pin', anchor: [0.5, 0.9] }
|
|
146
|
+
expect(getSymbolAnchor({ symbol: 'pin' }, symbolDef)).toEqual([0.5, 0.9])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('returns default [0.5, 0.5] when neither source has an anchor', () => {
|
|
150
|
+
expect(getSymbolAnchor({ symbol: 'pin' }, {})).toEqual([0.5, 0.5])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('returns default [0.5, 0.5] when symbolDef is undefined', () => {
|
|
154
|
+
expect(getSymbolAnchor({ symbol: 'pin' }, undefined)).toEqual([0.5, 0.5])
|
|
155
|
+
})
|
|
156
|
+
})
|
package/docs/examples.mdx
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import DemoMapInline from '../demo/DemoMapInline.js'
|
|
2
|
-
import DemoMapButton from '../demo/DemoMapButton.js'
|
|
3
|
-
|
|
4
|
-
# Examples
|
|
5
|
-
|
|
6
|
-
See [Getting started](getting-started) for installation and full configuration options.
|
|
7
|
-
|
|
8
|
-
## Inline map
|
|
9
|
-
|
|
10
|
-
Embed an interactive map directly on the page, allowing users to explore and interact with the map without leaving the current context.
|
|
11
|
-
|
|
12
|
-
<DemoMapInline />
|
|
13
|
-
|
|
14
|
-
```js
|
|
15
|
-
import InteractiveMap from '@defra/interactive-map'
|
|
16
|
-
import maplibreProvider from '@defra/interactive-map/providers/maplibre'
|
|
17
|
-
import searchPlugin from '@defra/interactive-map/plugins/search'
|
|
18
|
-
import scaleBarPlugin from '@defra/interactive-map/plugins/scale-bar'
|
|
19
|
-
import mapStylesPlugin from '@defra/interactive-map/plugins/map-styles'
|
|
20
|
-
|
|
21
|
-
new InteractiveMap('my-map', {
|
|
22
|
-
behaviour: 'inline',
|
|
23
|
-
mapProvider: maplibreProvider(),
|
|
24
|
-
mapStyle: {
|
|
25
|
-
url: '/assets/my-map-style.json',
|
|
26
|
-
attribution: '© OpenStreetMap contributors'
|
|
27
|
-
},
|
|
28
|
-
center: [-1.6, 53.1],
|
|
29
|
-
zoom: 6,
|
|
30
|
-
containerHeight: '500px',
|
|
31
|
-
enableZoomControls: true,
|
|
32
|
-
plugins: [
|
|
33
|
-
searchPlugin({ customDatasets: [nominatimDataset], showMarker: true }),
|
|
34
|
-
scaleBarPlugin({ units: 'metric' }),
|
|
35
|
-
mapStylesPlugin({ mapStyles: [...] })
|
|
36
|
-
]
|
|
37
|
-
})
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Button-triggered map
|
|
41
|
-
|
|
42
|
-
Trigger the map to show on button press, allowing users to access the map when needed without it taking up space on the page by default.
|
|
43
|
-
|
|
44
|
-
<DemoMapButton />
|
|
45
|
-
|
|
46
|
-
```js
|
|
47
|
-
import InteractiveMap from '@defra/interactive-map'
|
|
48
|
-
import maplibreProvider from '@defra/interactive-map/providers/maplibre'
|
|
49
|
-
import searchPlugin from '@defra/interactive-map/plugins/search'
|
|
50
|
-
import scaleBarPlugin from '@defra/interactive-map/plugins/scale-bar'
|
|
51
|
-
import mapStylesPlugin from '@defra/interactive-map/plugins/map-styles'
|
|
52
|
-
|
|
53
|
-
new InteractiveMap('my-map', {
|
|
54
|
-
behaviour: 'buttonFirst',
|
|
55
|
-
mapProvider: maplibreProvider(),
|
|
56
|
-
mapStyle: {
|
|
57
|
-
url: '/assets/my-map-style.json',
|
|
58
|
-
attribution: '© OpenStreetMap contributors'
|
|
59
|
-
},
|
|
60
|
-
center: [-1.6, 53.1],
|
|
61
|
-
zoom: 6,
|
|
62
|
-
containerHeight: '500px',
|
|
63
|
-
enableZoomControls: true,
|
|
64
|
-
plugins: [
|
|
65
|
-
searchPlugin({ customDatasets: [nominatimDataset], showMarker: true }),
|
|
66
|
-
scaleBarPlugin({ units: 'metric' }),
|
|
67
|
-
mapStylesPlugin({ mapStyles: [...] })
|
|
68
|
-
]
|
|
69
|
-
})
|
|
70
|
-
```
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { hasPattern, getPatternImageId, rasterisePattern } from '../../styles/patterns.js'
|
|
2
|
-
import { mergeSublayer } from '../../utils/mergeSublayer.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Collect all style configs that require a pattern image: top-level datasets
|
|
6
|
-
* and any sublayers whose merged style has a pattern.
|
|
7
|
-
* @param {Object[]} datasets
|
|
8
|
-
* @returns {Object[]}
|
|
9
|
-
*/
|
|
10
|
-
const getPatternConfigs = (datasets) =>
|
|
11
|
-
datasets.flatMap(dataset => {
|
|
12
|
-
const configs = hasPattern(dataset) ? [dataset] : []
|
|
13
|
-
if (dataset.sublayers?.length) {
|
|
14
|
-
dataset.sublayers.forEach(sublayer => {
|
|
15
|
-
const merged = mergeSublayer(dataset, sublayer)
|
|
16
|
-
if (hasPattern(merged)) {
|
|
17
|
-
configs.push(merged)
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
return configs
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Register all required pattern images with the map.
|
|
26
|
-
* Skips images that are already registered (safe to call on style change).
|
|
27
|
-
* @param {Object} map - MapLibre map instance
|
|
28
|
-
* @param {Object[]} datasets
|
|
29
|
-
* @param {string} mapStyleId
|
|
30
|
-
* @returns {Promise<void>}
|
|
31
|
-
*/
|
|
32
|
-
export const registerPatterns = async (map, datasets, mapStyleId) => {
|
|
33
|
-
const patternConfigs = getPatternConfigs(datasets)
|
|
34
|
-
if (!patternConfigs.length) {
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await Promise.all(patternConfigs.map(async (config) => {
|
|
39
|
-
const imageId = getPatternImageId(config, mapStyleId)
|
|
40
|
-
if (!imageId || map.hasImage(imageId)) {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
const result = await rasterisePattern(config, mapStyleId)
|
|
44
|
-
if (result) {
|
|
45
|
-
map.addImage(result.imageId, result.imageData, { pixelRatio: 2 })
|
|
46
|
-
}
|
|
47
|
-
}))
|
|
48
|
-
}
|