@defra/interactive-map 0.0.16-alpha → 0.0.18-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.
Files changed (224) hide show
  1. package/assets/images/slot-map.svg +264 -0
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/context.md +53 -7
  8. package/docs/api/map-style-config.md +41 -2
  9. package/docs/api/marker-config.md +53 -11
  10. package/docs/api/slots.md +16 -15
  11. package/docs/api/symbol-config.md +160 -0
  12. package/docs/api/symbol-registry.md +115 -0
  13. package/docs/api.md +25 -22
  14. package/docs/getting-started.md +4 -1
  15. package/docs/plugins/datasets.md +657 -0
  16. package/docs/plugins/interact.md +68 -43
  17. package/docs/plugins/search.md +15 -3
  18. package/docs/plugins.md +1 -1
  19. package/package.json +2 -2
  20. package/plugins/beta/datasets/dist/css/index.css +103 -15
  21. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  22. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  23. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  24. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  25. package/plugins/beta/datasets/src/DatasetsInit.jsx +29 -9
  26. package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
  27. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +159 -0
  28. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +75 -0
  29. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +440 -0
  30. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
  31. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
  32. package/plugins/beta/datasets/src/api/addDataset.js +2 -8
  33. package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
  34. package/plugins/beta/datasets/src/api/getStyle.js +13 -0
  35. package/plugins/beta/datasets/src/api/removeDataset.js +2 -44
  36. package/plugins/beta/datasets/src/api/setData.js +10 -0
  37. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
  38. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
  39. package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
  40. package/plugins/beta/datasets/src/api/setStyle.js +22 -0
  41. package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
  42. package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
  43. package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
  44. package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
  45. package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
  46. package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
  47. package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
  48. package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
  49. package/plugins/beta/datasets/src/datasets.js +39 -56
  50. package/plugins/beta/datasets/src/defaults.js +44 -8
  51. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +34 -25
  52. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
  53. package/plugins/beta/datasets/src/index.js +2 -1
  54. package/plugins/beta/datasets/src/manifest.js +25 -17
  55. package/plugins/beta/datasets/src/panels/Key.jsx +51 -51
  56. package/plugins/beta/datasets/src/panels/Key.module.scss +59 -9
  57. package/plugins/beta/datasets/src/panels/Layers.jsx +132 -29
  58. package/plugins/beta/datasets/src/panels/Layers.module.scss +56 -8
  59. package/plugins/beta/datasets/src/reducer.js +134 -9
  60. package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
  61. package/plugins/beta/datasets/src/utils/bbox.js +7 -5
  62. package/plugins/beta/datasets/src/utils/filters.js +5 -2
  63. package/plugins/beta/datasets/src/utils/mergeSublayer.js +86 -0
  64. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  65. package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
  66. package/plugins/beta/draw-ml/dist/css/index.css +21 -1
  67. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  68. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  69. package/plugins/beta/draw-ml/dist/umd/index.js +1 -1
  70. package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
  71. package/plugins/beta/draw-ml/src/draw.scss +0 -7
  72. package/plugins/beta/draw-ml/src/manifest.js +16 -16
  73. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  74. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  75. package/plugins/beta/frame/src/Frame.jsx +5 -5
  76. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  77. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  78. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  79. package/plugins/beta/map-styles/src/MapStyles.jsx +5 -4
  80. package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
  81. package/plugins/beta/map-styles/src/manifest.js +1 -1
  82. package/plugins/beta/scale-bar/dist/css/index.css +1 -1
  83. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
  84. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
  85. package/plugins/beta/scale-bar/src/index.test.js +3 -3
  86. package/plugins/beta/scale-bar/src/manifest.js +3 -3
  87. package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
  88. package/plugins/interact/dist/css/index.css +1 -1
  89. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  90. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  91. package/plugins/interact/dist/umd/index.js +1 -1
  92. package/plugins/interact/src/InteractInit.jsx +14 -5
  93. package/plugins/interact/src/InteractInit.test.js +26 -6
  94. package/plugins/interact/src/api/enable.test.js +7 -7
  95. package/plugins/interact/src/defaults.js +4 -6
  96. package/plugins/interact/src/events.js +9 -6
  97. package/plugins/interact/src/events.test.js +28 -4
  98. package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
  99. package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
  100. package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
  101. package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
  102. package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
  103. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
  104. package/plugins/interact/src/interact.scss +0 -7
  105. package/plugins/interact/src/manifest.js +14 -18
  106. package/plugins/interact/src/manifest.test.js +3 -1
  107. package/plugins/interact/src/reducer.js +23 -4
  108. package/plugins/interact/src/reducer.test.js +60 -11
  109. package/plugins/interact/src/utils/buildStylesMap.js +17 -4
  110. package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
  111. package/plugins/interact/src/utils/featureQueries.js +11 -6
  112. package/plugins/interact/src/utils/featureQueries.test.js +8 -1
  113. package/plugins/search/dist/css/index.css +1 -1
  114. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  115. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  116. package/plugins/search/src/Search.jsx +3 -1
  117. package/plugins/search/src/components/Form/Form.module.scss +2 -1
  118. package/plugins/search/src/events/fetchSuggestions.js +6 -4
  119. package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
  120. package/plugins/search/src/events/formHandlers.js +3 -3
  121. package/plugins/search/src/events/formHandlers.test.js +1 -1
  122. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  123. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  124. package/plugins/search/src/utils/updateMap.js +3 -3
  125. package/plugins/search/src/utils/updateMap.test.js +3 -3
  126. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  127. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  128. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  129. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  130. package/providers/maplibre/dist/umd/index.js +1 -1
  131. package/providers/maplibre/src/appEvents.js +7 -0
  132. package/providers/maplibre/src/appEvents.test.js +18 -4
  133. package/providers/maplibre/src/maplibreProvider.js +52 -0
  134. package/providers/maplibre/src/maplibreProvider.test.js +105 -1
  135. package/providers/maplibre/src/utils/highlightFeatures.js +37 -7
  136. package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -95
  137. package/providers/maplibre/src/utils/hoverCursor.js +61 -0
  138. package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
  139. package/providers/maplibre/src/utils/patternImages.js +70 -0
  140. package/providers/maplibre/src/utils/patternImages.test.js +180 -0
  141. package/providers/maplibre/src/utils/queryFeatures.js +38 -16
  142. package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
  143. package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
  144. package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
  145. package/providers/maplibre/src/utils/symbolImages.js +147 -0
  146. package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
  147. package/src/App/components/Actions/Actions.jsx +2 -2
  148. package/src/App/components/Actions/Actions.module.scss +0 -7
  149. package/src/App/components/Actions/Actions.test.jsx +1 -1
  150. package/src/App/components/Icon/Icon.jsx +3 -2
  151. package/src/App/components/Icon/Icon.module.scss +4 -0
  152. package/src/App/components/Icon/Icon.test.jsx +43 -4
  153. package/src/App/components/MapButton/MapButton.jsx +42 -17
  154. package/src/App/components/MapButton/MapButton.module.scss +4 -13
  155. package/src/App/components/MapButton/MapButton.test.jsx +27 -3
  156. package/src/App/components/Markers/Markers.jsx +122 -27
  157. package/src/App/components/Markers/Markers.module.scss +0 -10
  158. package/src/App/components/Markers/Markers.test.jsx +246 -0
  159. package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
  160. package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
  161. package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
  162. package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
  163. package/src/App/hooks/useButtonStateEvaluator.js +12 -2
  164. package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
  165. package/src/App/hooks/useInterfaceAPI.js +6 -0
  166. package/src/App/hooks/useInterfaceAPI.test.js +156 -0
  167. package/src/App/hooks/useLayoutMeasurements.js +84 -18
  168. package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
  169. package/src/App/hooks/useMarkersAPI.js +2 -5
  170. package/src/App/hooks/useMarkersAPI.test.js +4 -4
  171. package/src/App/layout/Layout.jsx +14 -9
  172. package/src/App/layout/Layout.test.jsx +6 -4
  173. package/src/App/layout/layout.module.scss +67 -29
  174. package/src/App/registry/pluginRegistry.js +1 -1
  175. package/src/App/renderer/HtmlElementHost.jsx +2 -1
  176. package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
  177. package/src/App/renderer/mapButtons.js +1 -1
  178. package/src/App/renderer/mapPanels.test.js +2 -2
  179. package/src/App/renderer/slotHelpers.js +2 -2
  180. package/src/App/renderer/slotHelpers.test.js +5 -5
  181. package/src/App/renderer/slots.js +9 -5
  182. package/src/App/store/AppProvider.jsx +3 -1
  183. package/src/App/store/AppProvider.test.jsx +1 -1
  184. package/src/App/store/ServiceProvider.jsx +8 -4
  185. package/src/App/store/appActionsMap.js +16 -0
  186. package/src/App/store/appActionsMap.test.js +27 -0
  187. package/src/App/store/appDispatchMiddleware.js +1 -1
  188. package/src/App/store/appDispatchMiddleware.test.js +2 -2
  189. package/src/App/store/appReducer.js +2 -0
  190. package/src/App/store/mapActionsMap.js +4 -6
  191. package/src/App/store/mapActionsMap.test.js +3 -2
  192. package/src/App/store/mapReducer.js +2 -1
  193. package/src/InteractiveMap/InteractiveMap.js +4 -0
  194. package/src/config/appConfig.js +5 -8
  195. package/src/config/appConfig.test.js +1 -2
  196. package/src/config/defaults.js +0 -2
  197. package/src/config/events.js +28 -0
  198. package/src/config/mapTheme.js +56 -0
  199. package/src/config/patternConfig.js +16 -0
  200. package/src/config/symbolConfig.js +80 -0
  201. package/src/scss/main.scss +1 -0
  202. package/src/scss/settings/_colors.scss +0 -9
  203. package/src/scss/settings/_dimensions.scss +0 -1
  204. package/src/services/patternRegistry.js +40 -0
  205. package/src/services/patternRegistry.test.js +48 -0
  206. package/src/services/symbolRegistry.js +113 -0
  207. package/src/services/symbolRegistry.test.js +262 -0
  208. package/src/types.js +93 -11
  209. package/src/utils/getSafeZoneInset.js +9 -7
  210. package/src/utils/getSafeZoneInset.test.js +10 -10
  211. package/src/utils/patternUtils.js +94 -0
  212. package/src/utils/patternUtils.test.js +160 -0
  213. package/src/utils/symbolUtils.js +85 -0
  214. package/src/utils/symbolUtils.test.js +156 -0
  215. package/webpack.dev.mjs +1 -1
  216. package/docs/api/slot-map.svg +0 -1
  217. package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
  218. package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
  219. package/plugins/beta/datasets/src/api/showDataset.js +0 -14
  220. package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
  221. package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
  222. package/plugins/beta/datasets/src/mapLayers.js +0 -164
  223. /package/src/{utils → services}/logger.js +0 -0
  224. /package/src/{utils → services}/logger.test.js +0 -0
@@ -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/webpack.dev.mjs CHANGED
@@ -18,7 +18,7 @@ export default {
18
18
  ],
19
19
  entry: {
20
20
  index: path.join(__dirname, 'demo/js/index.js'),
21
- forms: path.join(__dirname, 'demo/js/forms.js'),
21
+ draw: path.join(__dirname, 'demo/js/draw.js'),
22
22
  farming: path.join(__dirname, 'demo/js/farming.js'),
23
23
  planning: path.join(__dirname, 'demo/js/planning.js')
24
24
  },
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 838 629" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="1.5" xmlns:v="https://vecta.io/nano"><defs><style>@font-face{font-family:"CourierNewPS-BoldMT";src:url(data:application/font-woff;charset=utf-8;base64,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) format("woff"); font-weight:bold;font-style:normal;}</style></defs><style><![CDATA[.B{font-family:CourierNewPS-BoldMT, Courier New, monospace}.C{font-weight:700}.D{font-size:18px}.E{fill:#0969da}.F{font-family:ArialMT, Arial, sans-serif}.G{font-size:14px}.H{fill:#484949}]]></style><g fill="none" stroke="#d1d9e0"><path d="M171 121v327m496-327v327M171 284H1m836 0H667"/><path d="M1 61h836" stroke-width=".999"/><path d="M1 1h836v627H1z" stroke-width=".993"/><path d="M1 568h836M1 121l836 .001M279 61v60m280-60v60"/><path d="M1 448h836M1 508h836"/><path d="M251 508v-60"/><path d="M294 368h250v60H294z" stroke-width="1.001"/></g><text x="386.349" y="27.083" class="B C D E">banner</text><text x="383.347" y="43.182" class="F G H">mobile only</text><text x="381.44" y="594.177" class="B C D E">actions</text><text x="383.839" y="610.276" class="F G H">mobile only</text><text x="381.351" y="394.259" class="B C D E">actions</text><text x="375.571" y="410.358" class="F G H">tablet/desktop</text><text x="110.175" y="481.538" class="F G H">Logo</text><text x="386.195" y="534.177" class="B C D E">bottom</text><text x="383.193" y="550.276" class="F G H">mobile only</text><text x="96.226" y="94.819" class="B C D E">top-left</text><text x="364.776" y="94.819" class="B C D E">top-middle</text><text x="648.825" y="94.819" class="B C D E">top-right</text><text x="685.986" y="368.985" class="B C D E">right-bottom</text><text x="686.61" y="481.819" class="B C D E">footer-right</text><text x="25.795" y="371.554" class="B C D E">left-bottom</text><text x="702.664" y="203.591" class="B C D E">right-top</text><text x="42.472" y="206.319" class="B C D E">left-top</text></svg>
@@ -1,14 +0,0 @@
1
- export const hideDataset = ({ mapProvider, pluginState }, datasetId) => {
2
- const map = mapProvider.map
3
-
4
- // Update map layer visibility
5
- if (map.getLayer(datasetId)) {
6
- map.setLayoutProperty(datasetId, 'visibility', 'none')
7
- }
8
- if (map.getLayer(`${datasetId}-stroke`)) {
9
- map.setLayoutProperty(`${datasetId}-stroke`, 'visibility', 'none')
10
- }
11
-
12
- // Update state
13
- pluginState.dispatch({ type: 'SET_DATASET_VISIBILITY', payload: { id: datasetId, visibility: 'hidden' } })
14
- }
@@ -1,41 +0,0 @@
1
- import { applyExclusionFilter } from '../utils/filters.js'
2
-
3
- export const hideFeatures = ({ mapProvider, pluginState }, { featureIds, idProperty, datasetId }) => {
4
- const map = mapProvider.map
5
-
6
- // Get dataset to access original filter and determine layer IDs
7
- const dataset = pluginState.datasets?.find(d => d.id === datasetId)
8
- if (!dataset) {
9
- return
10
- }
11
-
12
- const originalFilter = dataset.filter || null
13
- const hasFill = !!dataset.fill
14
- const hasStroke = !!dataset.stroke
15
- const fillLayerId = hasFill ? datasetId : null
16
-
17
- let strokeLayerId = null
18
- if (hasStroke) {
19
- strokeLayerId = hasFill ? `${datasetId}-stroke` : datasetId
20
- }
21
-
22
- // Get current hidden state and calculate all hidden IDs
23
- const existingHidden = pluginState.hiddenFeatures[datasetId]
24
- const allHiddenIds = existingHidden
25
- ? [...new Set([...existingHidden.ids, ...featureIds])]
26
- : featureIds
27
-
28
- // Update state (store by datasetId, not individual layer IDs)
29
- pluginState.dispatch({
30
- type: 'HIDE_FEATURES',
31
- payload: { layerId: datasetId, idProperty, featureIds }
32
- })
33
-
34
- // Apply filter to both layers
35
- if (fillLayerId) {
36
- applyExclusionFilter(map, fillLayerId, originalFilter, idProperty, allHiddenIds)
37
- }
38
- if (strokeLayerId) {
39
- applyExclusionFilter(map, strokeLayerId, originalFilter, idProperty, allHiddenIds)
40
- }
41
- }
@@ -1,14 +0,0 @@
1
- export const showDataset = ({ mapProvider, pluginState }, datasetId) => {
2
- const map = mapProvider.map
3
-
4
- // Update map layer visibility
5
- if (map.getLayer(datasetId)) {
6
- map.setLayoutProperty(datasetId, 'visibility', 'visible')
7
- }
8
- if (map.getLayer(`${datasetId}-stroke`)) {
9
- map.setLayoutProperty(`${datasetId}-stroke`, 'visibility', 'visible')
10
- }
11
-
12
- // Update state
13
- pluginState.dispatch({ type: 'SET_DATASET_VISIBILITY', payload: { id: datasetId, visibility: 'visible' } })
14
- }
@@ -1,44 +0,0 @@
1
- import { applyExclusionFilter } from '../utils/filters.js'
2
-
3
- export const showFeatures = ({ mapProvider, pluginState }, { featureIds, idProperty, datasetId }) => {
4
- const map = mapProvider.map
5
-
6
- // Get current hidden state before update
7
- const existingHidden = pluginState.hiddenFeatures[datasetId]
8
- if (!existingHidden) {
9
- return
10
- }
11
-
12
- // Get dataset to access original filter and determine layer IDs
13
- const dataset = pluginState.datasets?.find(d => d.id === datasetId)
14
- if (!dataset) {
15
- return
16
- }
17
-
18
- const originalFilter = dataset.filter || null
19
- const hasFill = !!dataset.fill
20
- const hasStroke = !!dataset.stroke
21
- const fillLayerId = hasFill ? datasetId : null
22
-
23
- let strokeLayerId = null
24
- if (hasStroke) {
25
- strokeLayerId = hasFill ? `${datasetId}-stroke` : datasetId
26
- }
27
-
28
- // Calculate remaining hidden IDs
29
- const remainingHiddenIds = existingHidden.ids.filter(id => !featureIds.includes(id))
30
-
31
- // Update state
32
- pluginState.dispatch({
33
- type: 'SHOW_FEATURES',
34
- payload: { layerId: datasetId, featureIds }
35
- })
36
-
37
- // Apply filter to both layers (or restore original if nothing hidden)
38
- if (fillLayerId) {
39
- applyExclusionFilter(map, fillLayerId, originalFilter, idProperty, remainingHiddenIds)
40
- }
41
- if (strokeLayerId) {
42
- applyExclusionFilter(map, strokeLayerId, originalFilter, idProperty, remainingHiddenIds)
43
- }
44
- }
@@ -1,54 +0,0 @@
1
- import { addMapLayers } from './mapLayers.js'
2
- import { applyExclusionFilter } from './utils/filters.js'
3
-
4
- export const handleSetMapStyle = ({
5
- map,
6
- events,
7
- eventBus,
8
- getDatasets,
9
- getHiddenFeatures,
10
- getDynamicSources
11
- }) => {
12
- const onSetStyle = (e) => {
13
- map.once('idle', () => {
14
- const newStyleId = e.id
15
- const datasets = getDatasets()
16
- const hiddenFeatures = getHiddenFeatures()
17
- const dynamicSources = getDynamicSources ? getDynamicSources() : new Map()
18
-
19
- // Re-add all layers with correct colors for new style
20
- datasets.forEach(dataset => {
21
- addMapLayers(map, newStyleId, dataset)
22
- })
23
-
24
- // Reapply cached data for dynamic sources
25
- dynamicSources.forEach(source => {
26
- source.reapply()
27
- })
28
-
29
- // Reapply hidden features filters
30
- Object.entries(hiddenFeatures).forEach(([datasetId, { idProperty, ids }]) => {
31
- const dataset = datasets.find(d => d.id === datasetId)
32
- if (!dataset) {
33
- return
34
- }
35
-
36
- const originalFilter = dataset.filter || null
37
- const hasFill = !!dataset.fill
38
- const hasStroke = !!dataset.stroke
39
- const fillLayerId = hasFill ? datasetId : null
40
- const strokeLayerId = hasStroke ? (hasFill ? `${datasetId}-stroke` : datasetId) : null
41
-
42
- if (fillLayerId) {
43
- applyExclusionFilter(map, fillLayerId, originalFilter, idProperty, ids)
44
- }
45
- if (strokeLayerId) {
46
- applyExclusionFilter(map, strokeLayerId, originalFilter, idProperty, ids)
47
- }
48
- })
49
- })
50
- }
51
-
52
- eventBus.on(events.MAP_SET_STYLE, onSetStyle)
53
- return onSetStyle
54
- }