@defra/interactive-map 0.0.21-alpha → 0.0.23-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/esm/index.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/button-definition.md +39 -0
  8. package/docs/api/context.md +4 -4
  9. package/docs/api/map-style-config.md +11 -2
  10. package/docs/api/marker-config.md +38 -7
  11. package/docs/api/panel-definition.md +7 -3
  12. package/docs/api/symbol-config.md +10 -26
  13. package/docs/api/symbol-registry.md +28 -13
  14. package/docs/api.md +65 -4
  15. package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
  16. package/docs/assets/images/toggle-marker-label.jpg +0 -0
  17. package/docs/examples/index.mdx +12 -0
  18. package/docs/examples/toggle-marker-label.mdx +89 -0
  19. package/docs/plugins/datasets.md +39 -1
  20. package/docs/plugins/interact.md +4 -11
  21. package/docusaurus.config.cjs +2 -1
  22. package/package.json +10 -12
  23. package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -0
  24. package/plugins/beta/datasets/dist/css/index.css +2 -2
  25. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  26. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +2 -0
  27. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +2 -0
  28. package/plugins/beta/datasets/dist/umd/maplibre/index.js +2 -0
  29. package/plugins/beta/datasets/src/adapters/maplibre/index.js +1 -1
  30. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
  31. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
  32. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
  33. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
  34. package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
  35. package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
  36. package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
  37. package/plugins/beta/datasets/src/index.maplibre.umd.js +23 -0
  38. package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
  39. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  40. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  41. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  42. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  43. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  44. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  45. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  46. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  47. package/plugins/beta/use-location/src/manifest.js +1 -0
  48. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  49. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  50. package/plugins/interact/dist/umd/index.js +1 -1
  51. package/plugins/interact/src/InteractInit.jsx +23 -55
  52. package/plugins/interact/src/InteractInit.test.js +58 -80
  53. package/plugins/interact/src/defaults.js +1 -3
  54. package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
  55. package/plugins/interact/src/hooks/useAttachEvents.test.js +65 -0
  56. package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
  57. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
  58. package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
  59. package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
  60. package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
  61. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
  62. package/plugins/interact/src/hooks/useMapItemList.js +187 -0
  63. package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
  64. package/plugins/interact/src/index.js +0 -2
  65. package/plugins/interact/src/index.test.js +0 -2
  66. package/plugins/interact/src/manifest.js +37 -6
  67. package/plugins/interact/src/reducer.js +8 -4
  68. package/plugins/interact/src/reducer.test.js +18 -3
  69. package/plugins/interact/src/utils/buildStylesMap.js +21 -10
  70. package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
  71. package/plugins/interact/src/utils/spatial.js +0 -15
  72. package/plugins/interact/src/utils/spatial.test.js +1 -13
  73. package/plugins/search/dist/css/index.css +1 -1
  74. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  75. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  76. package/plugins/search/src/components/CloseButton/CloseButton.module.scss +2 -0
  77. package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
  78. package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
  79. package/plugins/search/src/search.scss +1 -0
  80. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  81. package/providers/beta/esri/src/esriProvider.js +4 -0
  82. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  83. package/providers/maplibre/dist/esm/index.js +1 -1
  84. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  85. package/providers/maplibre/dist/umd/index.js +1 -1
  86. package/providers/maplibre/src/index.js +7 -1
  87. package/providers/maplibre/src/index.test.js +15 -2
  88. package/providers/maplibre/src/mapEvents.js +11 -5
  89. package/providers/maplibre/src/mapEvents.test.js +14 -3
  90. package/providers/maplibre/src/maplibreProvider.js +18 -11
  91. package/providers/maplibre/src/maplibreProvider.test.js +43 -36
  92. package/providers/maplibre/src/utils/highlightFeatures.js +102 -57
  93. package/providers/maplibre/src/utils/highlightFeatures.test.js +162 -51
  94. package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
  95. package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
  96. package/providers/maplibre/src/utils/patternImages.js +10 -12
  97. package/providers/maplibre/src/utils/patternImages.test.js +145 -167
  98. package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
  99. package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
  100. package/providers/maplibre/src/utils/symbolImages.js +23 -86
  101. package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
  102. package/rollup.esm.mjs +1 -1
  103. package/src/App/components/Actions/Actions.module.scss +1 -0
  104. package/src/App/components/Actions/Actions.test.jsx +17 -4
  105. package/src/App/components/Hints/Hints.jsx +52 -0
  106. package/src/App/components/Hints/Hints.module.scss +37 -0
  107. package/src/App/components/Hints/Hints.test.jsx +104 -0
  108. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
  109. package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
  110. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
  111. package/src/App/components/Markers/LabelMarker.jsx +14 -0
  112. package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
  113. package/src/App/components/Markers/Markers.jsx +44 -75
  114. package/src/App/components/Markers/Markers.module.scss +58 -0
  115. package/src/App/components/Markers/Markers.test.jsx +90 -113
  116. package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
  117. package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
  118. package/src/App/components/Markers/SymbolMarker.jsx +31 -0
  119. package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
  120. package/src/App/components/Panel/Panel.jsx +1 -1
  121. package/src/App/components/Panel/Panel.module.scss +1 -0
  122. package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
  123. package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
  124. package/src/App/components/Tabs/Tabs.jsx +68 -0
  125. package/src/App/components/Tabs/Tabs.module.scss +57 -0
  126. package/src/App/components/Tabs/Tabs.test.jsx +121 -0
  127. package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
  128. package/src/App/components/Viewport/Features.jsx +35 -0
  129. package/src/App/components/Viewport/Features.module.scss +31 -0
  130. package/src/App/components/Viewport/Features.test.jsx +124 -0
  131. package/src/App/components/Viewport/MapController.jsx +1 -1
  132. package/src/App/components/Viewport/MapStatus.jsx +0 -4
  133. package/src/App/components/Viewport/Viewport.jsx +35 -29
  134. package/src/App/components/Viewport/Viewport.module.scss +7 -21
  135. package/src/App/components/Viewport/Viewport.test.jsx +108 -65
  136. package/src/App/controls/keyboardActions.js +9 -3
  137. package/src/App/controls/keyboardActions.test.js +122 -87
  138. package/src/App/controls/keyboardShortcuts.js +1 -14
  139. package/src/App/hooks/useFeatureFocus.js +199 -0
  140. package/src/App/hooks/useFeatureFocus.test.js +635 -0
  141. package/src/App/hooks/useFeatureItems.js +30 -0
  142. package/src/App/hooks/useFeatureItems.test.js +97 -0
  143. package/src/App/hooks/useFocusVisible.js +4 -7
  144. package/src/App/hooks/useFocusVisible.test.js +6 -6
  145. package/src/App/hooks/useKeyboardHint.js +9 -34
  146. package/src/App/hooks/useKeyboardHint.test.js +34 -84
  147. package/src/App/hooks/useKeyboardShortcuts.js +8 -5
  148. package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
  149. package/src/App/hooks/useLayoutMeasurements.js +10 -4
  150. package/src/App/hooks/useLayoutMeasurements.test.js +30 -0
  151. package/src/App/hooks/useMapURLSync.js +3 -3
  152. package/src/App/hooks/useMapURLSync.test.js +20 -3
  153. package/src/App/hooks/useMarkerCursor.js +33 -0
  154. package/src/App/hooks/useMarkerCursor.test.js +87 -0
  155. package/src/App/hooks/useMarkersAPI.js +32 -9
  156. package/src/App/hooks/useMarkersAPI.test.js +162 -94
  157. package/src/App/layout/Layout.jsx +2 -0
  158. package/src/App/layout/Layout.test.jsx +3 -0
  159. package/src/App/registry/keyboardShortcutRegistry.js +3 -2
  160. package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
  161. package/src/App/renderer/mapButtons.js +5 -1
  162. package/src/App/renderer/mapButtons.test.js +37 -1
  163. package/src/App/store/ServiceProvider.jsx +4 -1
  164. package/src/App/store/ServiceProvider.test.jsx +11 -0
  165. package/src/App/store/appActionsMap.js +5 -1
  166. package/src/App/store/appActionsMap.test.js +13 -0
  167. package/src/App/store/appReducer.js +1 -0
  168. package/src/App/store/mapActionsMap.js +2 -1
  169. package/src/InteractiveMap/InteractiveMap.js +13 -1
  170. package/src/InteractiveMap/InteractiveMap.test.js +6 -4
  171. package/src/InteractiveMap/behaviourController.js +1 -2
  172. package/src/InteractiveMap/buttonManager.js +1 -3
  173. package/src/InteractiveMap/buttonManager.test.js +16 -8
  174. package/src/InteractiveMap/domStateManager.js +2 -3
  175. package/src/InteractiveMap/historyManager.js +4 -3
  176. package/src/config/appConfig.js +3 -1
  177. package/src/config/appConfig.test.js +4 -0
  178. package/src/config/defaults.js +3 -2
  179. package/src/config/events.js +8 -0
  180. package/src/config/mapTheme.js +8 -4
  181. package/src/config/mergeConfig.js +8 -2
  182. package/src/config/mergeConfig.test.js +19 -0
  183. package/src/config/symbolConfig.js +23 -16
  184. package/src/scss/main.scss +3 -0
  185. package/src/scss/tools/_border-focus.scss +0 -1
  186. package/src/services/eventBus.js +15 -0
  187. package/src/services/eventBus.test.js +11 -0
  188. package/src/services/hints.js +47 -0
  189. package/src/services/hints.test.js +110 -0
  190. package/src/services/patternRegistry.js +69 -1
  191. package/src/services/patternRegistry.test.js +112 -1
  192. package/src/services/reverseGeocode.js +4 -0
  193. package/src/services/reverseGeocode.test.js +11 -1
  194. package/src/services/symbolRegistry.js +152 -17
  195. package/src/services/symbolRegistry.test.js +141 -31
  196. package/src/types.js +93 -16
  197. package/src/utils/constrainKeyboardFocus.js +5 -5
  198. package/src/utils/detectInterfaceType.js +27 -5
  199. package/src/utils/detectInterfaceType.test.js +29 -3
  200. package/src/utils/mapStateSync.js +2 -2
  201. package/src/utils/mapStateSync.test.js +18 -0
  202. package/src/utils/patternUtils.js +4 -67
  203. package/src/utils/patternUtils.test.js +1 -123
  204. package/src/utils/symbolUtils.js +11 -13
  205. package/src/utils/symbolUtils.test.js +18 -26
  206. package/webpack.dev.mjs +4 -0
  207. package/webpack.umd.mjs +9 -6
  208. package/plugins/beta/datasets/adapters/maplibre/dist/esm/im-datasets-ml-adapter.js +0 -1
  209. package/plugins/beta/datasets/adapters/maplibre/dist/umd/im-datasets-ml-adapter.js +0 -2
  210. package/plugins/beta/datasets/adapters/maplibre/dist/umd/index.js +0 -1
  211. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +0 -2
  212. package/plugins/beta/datasets/dist/umd/index.js +0 -2
  213. package/plugins/interact/dist/css/index.css +0 -1
  214. package/plugins/interact/src/interact.scss +0 -24
  215. /package/plugins/beta/datasets/{adapters/maplibre/dist → dist/adapters/maplibre}/esm/index.js +0 -0
  216. /package/plugins/beta/datasets/{adapters/maplibre/dist/umd/im-datasets-ml-adapter.js.LICENSE.txt → dist/umd/maplibre/im-datasets-maplibre-adapter.js.LICENSE.txt} +0 -0
  217. /package/plugins/beta/datasets/dist/umd/{im-datasets-plugin.js.LICENSE.txt → maplibre/im-datasets-plugin.js.LICENSE.txt} +0 -0
  218. /package/plugins/beta/datasets/dist/umd/{index.js.LICENSE.txt → maplibre/index.js.LICENSE.txt} +0 -0
package/src/types.js CHANGED
@@ -91,6 +91,41 @@
91
91
  * Core services (announce, reverseGeocode, closeApp, etc.).
92
92
  */
93
93
 
94
+ /**
95
+ * Defines an item in a button's popup menu.
96
+ *
97
+ * @typedef {Object} MenuItemDefinition
98
+ *
99
+ * @property {string} id
100
+ * Unique item identifier. Used to control state via toggleButtonState().
101
+ *
102
+ * @property {string} label
103
+ * Display text for the item.
104
+ *
105
+ * @property {string} [iconId]
106
+ * Icon identifier from the icon registry.
107
+ *
108
+ * @property {string} [iconSvgContent]
109
+ * Raw SVG content for the item icon. The outer SVG tag should be excluded.
110
+ *
111
+ * @property {boolean} [isPressed]
112
+ * Initial checked state. When set, the item renders as menuitemcheckbox.
113
+ *
114
+ * @property {boolean} [keepFocus=false]
115
+ * When true, focus returns to the menu's trigger button after selection instead of
116
+ * moving to the panel (if panelId is set) or the map viewport.
117
+ *
118
+ * @property {(e: MouseEvent) => void} [onClick]
119
+ * Click handler. Receives the native event. Not called when panelId is set.
120
+ *
121
+ * @property {string} [panelId]
122
+ * Associated panel identifier. When set, selecting the item opens the panel and moves
123
+ * focus to it. onClick is not called.
124
+ *
125
+ * @property {(context: PluginContext) => boolean} [pressedWhen]
126
+ * Reactive callback to determine if the item should appear checked. Plugin buttons only.
127
+ */
128
+
94
129
  /**
95
130
  * Defines a button that can be rendered in the UI at various breakpoints.
96
131
  *
@@ -133,12 +168,21 @@
133
168
  * @property {string | (() => string)} label
134
169
  * Accesible label. Text or a function returning the text. Used for the label or tooltip if 'showLabel' is false.
135
170
  *
171
+ * @property {MenuItemDefinition[]} [menuItems]
172
+ * Items for the button's popup menu. When provided, the button acts as a menu trigger.
173
+ *
136
174
  * @property {ButtonBreakpointConfig} mobile
137
175
  * Mobile breakpoint configuration.
138
176
  *
139
177
  * @property {(event: MouseEvent, context: PluginContext) => void} [onClick]
140
178
  * Click handler for the button.
141
179
  *
180
+ * @property {boolean} [keepFocus=false]
181
+ * When true, focus remains on this button after activation instead of moving to the panel or viewport.
182
+ * Use for repeated incremental actions (e.g. zoom) where the user may activate the button
183
+ * multiple times. Toggle buttons (isPressed/pressedWhen) always keep focus regardless of this flag.
184
+ * When combined with panelId, the panel opens but focus stays on the button.
185
+ *
142
186
  * @property {string} [panelId]
143
187
  * Associated panel identifier to toggle open when clicked.
144
188
  *
@@ -221,6 +265,10 @@
221
265
  * @property {(point: { x: number, y: number }) => any[]} getFeaturesAtPoint
222
266
  * Query rendered features at a screen pixel position (x from left edge, y from top edge of viewport).
223
267
  *
268
+ * @property {(layerIds: string[]) => any[]} getVisibleFeatures
269
+ * Return all GeoJSON features currently visible in the viewport on the given layers,
270
+ * respecting layer visibility and any active filters. Returns [] if unsupported.
271
+ *
224
272
  * @property {() => string} getAreaDimensions
225
273
  * Get the dimensions of the visible map area as a formatted string (e.g., '400m by 750m').
226
274
  *
@@ -236,7 +284,7 @@
236
284
  * @property {(point: { x: number, y: number }) => [number, number]} screenToMap
237
285
  * Convert screen pixel position (x from left edge, y from top edge of viewport) to map coordinates ([lng, lat] or [easting, northing] depending on the crs of the map provider).
238
286
  *
239
- * @property {(selectedFeatures: any[], stylesMap: any) => any} [updateHighlightedFeatures]
287
+ * @property {(selectedFeatures: any[], activeFeatures: any[], stylesMap: any) => any} [updateHighlightedFeatures]
240
288
  * @experimental Update highlighted features on the map.
241
289
  *
242
290
  * @property {(direction: string) => any} [highlightNextLabel]
@@ -262,6 +310,19 @@
262
310
  * Coordinate reference system.
263
311
  */
264
312
 
313
+ /**
314
+ * Configuration options for the MapLibre provider.
315
+ *
316
+ * @typedef {Object} MaplibreProviderConfig
317
+ *
318
+ * @property {string} [workerUrl]
319
+ * URL to a separately hosted MapLibre worker file (`maplibre-gl-csp-worker.js`).
320
+ * Required when running under a Content Security Policy that blocks `blob:` worker
321
+ * URLs. The file ships with maplibre-gl and must be served from your own origin.
322
+ * When set, MapLibre loads the worker from this URL instead of generating an
323
+ * inline blob, so your CSP only needs `worker-src 'self'`.
324
+ */
325
+
265
326
  /**
266
327
  * Descriptor for lazy-loading a map provider.
267
328
  *
@@ -316,7 +377,7 @@
316
377
  * Alt text for logo.
317
378
  *
318
379
  * @property {'light' | 'dark'} [mapColorScheme]
319
- * Map colour scheme. Sets the default values of `haloColor`, `selectedColor`, and `foregroundColor`
380
+ * Map colour scheme. Sets the default values of `haloColor`, `selectedColor`, `activeColor`, and `foregroundColor`
320
381
  * when not explicitly provided, and signals to map overlay components which tonal range to use.
321
382
  * `'light'` (default): dark overlays on a light basemap. `'dark'`: light overlays on a dark or aerial basemap.
322
383
  *
@@ -329,10 +390,14 @@
329
390
  * Injected as the `--map-overlay-halo-color` CSS custom property.
330
391
  *
331
392
  * @property {string} [selectedColor]
332
- * Theme colour for selected state — used by map overlay components to indicate a selected feature.
393
+ * Theme colour for committed selection — used by map overlay components to indicate a selected feature.
333
394
  * Falls back to `#0b0c0c` (light) or `#ffffff` (dark).
334
395
  * Injected as the `--map-overlay-selected-color` CSS custom property.
335
396
  *
397
+ * @property {string} [activeColor]
398
+ * Theme colour for the active (keyboard cursor) state — the focus ring shown when navigating features
399
+ * with the keyboard. Falls back to `#ffdd00` for both light and dark schemes.
400
+ *
336
401
  * @property {string} [foregroundColor]
337
402
  * Foreground colour for elements rendered on top of the map (e.g. text or iconography in overlay components).
338
403
  * Falls back to `#0b0c0c` (light) or `#ffffff` (dark).
@@ -354,7 +419,7 @@
354
419
  * @property {string} [symbolSvgContent]
355
420
  * Default inner SVG path content. When set, overrides `symbol`.
356
421
  *
357
- * @property {string} [viewBox='0 0 38 38']
422
+ * @property {string} [viewBox='0 0 44 44']
358
423
  * Default SVG viewBox.
359
424
  *
360
425
  * @property {[number, number]} [anchor=[0.5, 0.5]]
@@ -366,15 +431,10 @@
366
431
  * @property {string | Record<string, string>} [foregroundColor='#ffffff']
367
432
  * Default foreground fill colour.
368
433
  *
369
- * @property {string} [haloWidth='1']
370
- * Default halo stroke width in SVG units.
371
- *
372
434
  * @property {string} [graphic]
373
435
  * Default SVG `d` attribute value for the foreground graphic path. Each built-in symbol sets
374
436
  * its own default (a small dot). Override to swap the graphic across all markers globally.
375
437
  *
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
438
  */
379
439
 
380
440
  /**
@@ -407,8 +467,8 @@
407
467
  * When set, `symbol` is ignored.
408
468
  *
409
469
  * @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'`.
470
+ * SVG viewBox attribute for the symbol, e.g. `'0 0 44 44'`.
471
+ * Defaults to the registered symbol's viewBox, or `'0 0 44 44'`.
412
472
  *
413
473
  * @property {[number, number]} [anchor]
414
474
  * Anchor point as a normalised [x, y] pair where [0, 0] is top-left and [1, 1] is bottom-right.
@@ -421,9 +481,6 @@
421
481
  * @property {string | Record<string, string>} [foregroundColor]
422
482
  * Foreground fill colour of the symbol (e.g. the inner dot on a pin).
423
483
  *
424
- * @property {string} [haloWidth]
425
- * Stroke width of the halo in SVG units. Defaults to `'1'`.
426
- *
427
484
  * @property {string} [graphic]
428
485
  * SVG `d` attribute value for the foreground graphic path. Replaces the foreground shape of the
429
486
  * symbol while keeping its background, halo and selection ring intact. Each built-in symbol
@@ -434,6 +491,14 @@
434
491
  * import { graphics } from './config/symbolConfig.js'
435
492
  * markers.add('id', coords, { symbol: 'pin', graphic: graphics.cross })
436
493
  *
494
+ * @property {string} [label]
495
+ * Plain-text label for this marker. Used as the accessible name on the marker SVG, and rendered
496
+ * as a visible label bubble when `showLabel` is `true` or when `symbol` is `null` (standalone label).
497
+ *
498
+ * @property {boolean} [showLabel=false]
499
+ * Whether to render a visible label bubble above the marker symbol. Defaults to `false`.
500
+ * Set `symbol` to `null` instead to create a standalone label with no symbol.
501
+ *
437
502
  */
438
503
 
439
504
  /**
@@ -444,6 +509,11 @@
444
509
  * @property {PanelBreakpointConfig} desktop
445
510
  * Desktop breakpoint configuration.
446
511
  *
512
+ * @property {boolean} [focus=true]
513
+ * Whether to move focus to the panel when it opens. Set to false to prevent the panel from
514
+ * receiving focus — useful for panels present on page load or panels that should not interrupt
515
+ * the user's current flow. Modal panels always receive focus regardless of this setting.
516
+ *
447
517
  * @property {string} [html]
448
518
  * HTML content.
449
519
  *
@@ -621,8 +691,15 @@
621
691
  * @property {MapStyleConfig} [mapStyle]
622
692
  * Map style configuration.
623
693
  *
624
- * @property {string} [mapViewParamKey='mv']
625
- * URL query parameter key used to control map view state.
694
+ * @property {string} [mapViewQueryParam='mv']
695
+ * URL query parameter used to control fullscreen/hybrid/buttonFirst state.
696
+ * Override if the default value clashes with an existing parameter on your page.
697
+ *
698
+ * @property {'sync' | 'readOnly' | 'none'} [urlPosition='sync']
699
+ * Controls how map center and zoom interact with the page URL.
700
+ * - `'sync'` — reads on load and writes on pan/zoom (default)
701
+ * - `'readOnly'` — seeds the initial view from the URL but never writes back
702
+ * - `'none'` — ignores the URL entirely; use when you don't want the user's pan/zoom to be persisted or shared
626
703
  *
627
704
  * @property {MarkerConfig[]} [markers]
628
705
  * Initial markers to display on the map.
@@ -4,11 +4,11 @@ export function constrainKeyboardFocus (containerEl, e) {
4
4
  }
5
5
 
6
6
  const selectors = [
7
- 'a[href]:not([disabled])',
8
- 'button:not([disabled])',
9
- 'textarea:not([disabled])',
10
- 'input:not([disabled])',
11
- 'select:not([disabled])',
7
+ 'a[href]:not([disabled]):not([tabindex="-1"])',
8
+ 'button:not([disabled]):not([tabindex="-1"])',
9
+ 'textarea:not([disabled]):not([tabindex="-1"])',
10
+ 'input:not([disabled]):not([tabindex="-1"])',
11
+ 'select:not([disabled]):not([tabindex="-1"])',
12
12
  '*[tabindex="0"]:not([disabled])'
13
13
  ]
14
14
  let focusableEls = Array.from(containerEl.querySelectorAll(selectors.join(',')))
@@ -1,5 +1,6 @@
1
1
  let lastInterfaceType = window.matchMedia('(pointer: coarse)').matches ? 'touch' : 'unknown'
2
2
  const interfaceTypeListeners = new Set()
3
+ const interfaceTypeImmediateListeners = new Set()
3
4
 
4
5
  // -----------------------------------------------------------------------------
5
6
  // Internal (not exported)
@@ -20,9 +21,8 @@ function normalizePointerType (pointerType) {
20
21
  function notifyListeners (newType) {
21
22
  if (lastInterfaceType !== newType) {
22
23
  lastInterfaceType = newType
23
- interfaceTypeListeners.forEach((listener) => {
24
- listener(newType)
25
- })
24
+ interfaceTypeImmediateListeners.forEach(listener => listener(newType))
25
+ interfaceTypeListeners.forEach(listener => listener(newType))
26
26
  }
27
27
  }
28
28
 
@@ -44,7 +44,18 @@ function createInterfaceDetector () {
44
44
 
45
45
  const handlePointer = event => {
46
46
  const type = normalizePointerType(event.pointerType)
47
- setTimeout(() => notifyListeners(type), REACT_CLICK_DELAY)
47
+ if (type === lastInterfaceType) {
48
+ return
49
+ }
50
+ // Update synchronously so getInterfaceType() returns the new value immediately —
51
+ // this prevents focusin handlers from seeing the stale 'keyboard' type during a
52
+ // pointer-triggered focus move. Listeners (React state) are still notified async
53
+ // to avoid layout thrashing during the click event.
54
+ lastInterfaceType = type
55
+ interfaceTypeImmediateListeners.forEach(listener => listener(type))
56
+ setTimeout(() => {
57
+ interfaceTypeListeners.forEach(listener => listener(type))
58
+ }, REACT_CLICK_DELAY)
48
59
  }
49
60
 
50
61
  const handleKeyDown = e => {
@@ -81,8 +92,19 @@ function subscribeToInterfaceChanges (onInterfaceTypeChange) {
81
92
  }
82
93
  }
83
94
 
95
+ // Fires synchronously within the same event cycle — use for direct DOM updates
96
+ // that need to be in sync with the pointer event (no 150ms React delay).
97
+ function subscribeToInterfaceChangesImmediate (onInterfaceTypeChange) {
98
+ interfaceTypeImmediateListeners.add(onInterfaceTypeChange)
99
+
100
+ return () => {
101
+ interfaceTypeImmediateListeners.delete(onInterfaceTypeChange)
102
+ }
103
+ }
104
+
84
105
  export {
85
106
  createInterfaceDetector,
86
107
  getInterfaceType,
87
- subscribeToInterfaceChanges
108
+ subscribeToInterfaceChanges,
109
+ subscribeToInterfaceChangesImmediate
88
110
  }
@@ -54,7 +54,7 @@ const triggerDomEvent = (event, payload) => {
54
54
 
55
55
  // --- TEST SUITE ---
56
56
  describe('Interface Detector Utility Module', () => {
57
- let createInterfaceDetector, getInterfaceType, subscribeToInterfaceChanges
57
+ let createInterfaceDetector, getInterfaceType, subscribeToInterfaceChanges, subscribeToInterfaceChangesImmediate
58
58
  let cleanup
59
59
 
60
60
  beforeEach(async () => {
@@ -72,6 +72,7 @@ describe('Interface Detector Utility Module', () => {
72
72
  createInterfaceDetector = importedModule.createInterfaceDetector
73
73
  getInterfaceType = importedModule.getInterfaceType
74
74
  subscribeToInterfaceChanges = importedModule.subscribeToInterfaceChanges
75
+ subscribeToInterfaceChangesImmediate = importedModule.subscribeToInterfaceChangesImmediate
75
76
 
76
77
  Object.keys(eventListeners).forEach(key => delete eventListeners[key])
77
78
  Object.keys(mediaListeners).forEach(key => delete mediaListeners[key])
@@ -99,18 +100,21 @@ describe('Interface Detector Utility Module', () => {
99
100
 
100
101
  // --- Path 1a: pointerdown 'touch' ---
101
102
  triggerDomEvent('pointerdown', { pointerType: 'touch' })
103
+ expect(getInterfaceType()).toBe('touch') // synchronous update
104
+ expect(handler).not.toHaveBeenCalled() // listener still pending
102
105
  jest.advanceTimersByTime(150)
103
- expect(getInterfaceType()).toBe('touch')
104
106
  expect(handler).toHaveBeenCalledWith('touch')
105
107
  handler.mockClear()
106
108
 
107
109
  // --- Path 1b: pointerdown 'pen' ---
108
110
  triggerDomEvent('pointerdown', { pointerType: 'pen' })
109
111
  jest.advanceTimersByTime(150)
110
- expect(handler).not.toHaveBeenCalled()
112
+ expect(handler).not.toHaveBeenCalled() // 'pen' normalises to 'touch' — no change
111
113
 
112
114
  // --- Path 1c: pointerdown 'mouse' ---
113
115
  triggerDomEvent('pointerdown', { pointerType: 'mouse' })
116
+ expect(getInterfaceType()).toBe('mouse') // synchronous update
117
+ expect(handler).not.toHaveBeenCalled() // listener still pending
114
118
  jest.advanceTimersByTime(150)
115
119
  expect(getInterfaceType()).toBe('mouse')
116
120
  expect(handler).toHaveBeenCalledWith('mouse')
@@ -129,6 +133,10 @@ describe('Interface Detector Utility Module', () => {
129
133
  expect(handler).toHaveBeenCalledWith('keyboard')
130
134
  handler.mockClear()
131
135
 
136
+ // Tab again when already keyboard — notifyListeners no-op branch
137
+ triggerDomEvent('keydown', { key: 'Tab' })
138
+ expect(handler).not.toHaveBeenCalled()
139
+
132
140
  // keydown with non-Tab
133
141
  triggerDomEvent('keydown', { key: 'a' })
134
142
  expect(handler).not.toHaveBeenCalled()
@@ -152,6 +160,24 @@ describe('Interface Detector Utility Module', () => {
152
160
  expect(handler).not.toHaveBeenCalled()
153
161
  })
154
162
 
163
+ it('subscribeToInterfaceChangesImmediate fires synchronously and unsubscribes', () => {
164
+ const handler = jest.fn()
165
+ const unsubscribe = subscribeToInterfaceChangesImmediate(handler)
166
+ cleanup = createInterfaceDetector()
167
+
168
+ triggerDomEvent('pointerdown', { pointerType: 'touch' })
169
+ expect(handler).toHaveBeenCalledWith('touch') // fires before timer advance
170
+
171
+ // Tab keydown goes through notifyListeners — exercises immediate forEach callback
172
+ triggerDomEvent('keydown', { key: 'Tab' })
173
+ expect(handler).toHaveBeenCalledWith('keyboard')
174
+ expect(handler).toHaveBeenCalledTimes(2)
175
+
176
+ unsubscribe()
177
+ triggerDomEvent('pointerdown', { pointerType: 'mouse' })
178
+ expect(handler).toHaveBeenCalledTimes(2) // no longer called after unsubscribe
179
+ })
180
+
155
181
  it('should return "touch" when matchMedia initially matches coarse pointer', async () => {
156
182
  jest.resetModules()
157
183
 
@@ -72,8 +72,8 @@ const setMapStateInURL = (id, state, currentHref = window.location.href) => {
72
72
  * @param {string} [search] - URL search string. Defaults to `window.location.search`.
73
73
  * @returns {{ center: [number, number], zoom: number } | { bounds: any }}
74
74
  */
75
- const getInitialMapState = ({ id, center, zoom, bounds }, search = window.location.search) => {
76
- const savedState = getMapStateFromURL(id, search)
75
+ const getInitialMapState = ({ id, center, zoom, bounds, urlPosition = 'sync' }, search = window.location.search) => {
76
+ const savedState = urlPosition === 'none' ? null : getMapStateFromURL(id, search)
77
77
  if (savedState) {
78
78
  return {
79
79
  center: savedState.center,
@@ -30,6 +30,24 @@ describe('mapStateSync utilities', () => {
30
30
  const state = getInitialMapState({ id: 'map1', bounds }, '')
31
31
  expect(state.bounds).toEqual(bounds)
32
32
  })
33
+
34
+ it('ignores URL params when urlPosition is none', () => {
35
+ const state = getInitialMapState(
36
+ { id: 'map1', center: [1, 2], zoom: 3, urlPosition: 'none' },
37
+ '?map1:center=10,20&map1:zoom=5'
38
+ )
39
+ expect(state.center).toEqual([1, 2])
40
+ expect(state.zoom).toBe(3)
41
+ })
42
+
43
+ it('reads URL params when urlPosition is readOnly', () => {
44
+ const state = getInitialMapState(
45
+ { id: 'map1', center: [1, 2], zoom: 3, urlPosition: 'readOnly' },
46
+ '?map1:center=10,20&map1:zoom=5'
47
+ )
48
+ expect(state.center).toEqual([10, 20])
49
+ expect(state.zoom).toBe(5)
50
+ })
33
51
  })
34
52
 
35
53
  describe('setMapStateInURL', () => {
@@ -1,7 +1,9 @@
1
- import { getValueForStyle } from '../utils/getValueForStyle.js'
2
-
3
1
  // Border path rendered behind the pattern content in Key panel symbols (20×20 coordinate space).
4
2
  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"/>'
3
+ // Minimum oversampling — keeps 16×16 physical pixels as the floor so patterns remain crisp.
4
+ const PATTERN_MIN_PIXEL_RATIO = 2
5
+
6
+ export const getEffectivePixelRatio = (pixelRatio) => Math.max(PATTERN_MIN_PIXEL_RATIO, pixelRatio * 2)
5
7
 
6
8
  export const hashString = (str) => {
7
9
  let hash = 0
@@ -32,68 +34,3 @@ export const injectColors = (content, foregroundColor, backgroundColor) =>
32
34
  * @returns {boolean}
33
35
  */
34
36
  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 + pixel ratio combination.
56
- *
57
- * @param {Object} dataset
58
- * @param {string} mapStyleId
59
- * @param {Object} patternRegistry
60
- * @param {number} [pixelRatio=1]
61
- * @returns {string|null}
62
- */
63
- // Minimum oversampling — keeps 16×16 physical pixels as the floor so patterns remain crisp.
64
- export const PATTERN_MIN_PIXEL_RATIO = 2
65
-
66
- export const getPatternImageId = (dataset, mapStyleId, patternRegistry, pixelRatio = 1) => {
67
- const innerContent = getPatternInnerContent(dataset, patternRegistry)
68
- if (!innerContent) {
69
- return null
70
- }
71
- const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
72
- const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
73
- const effectiveRatio = Math.max(PATTERN_MIN_PIXEL_RATIO, pixelRatio)
74
- return `pattern-${hashString(innerContent + fg + bg)}-${effectiveRatio}x`
75
- }
76
-
77
- /**
78
- * Returns colour-injected SVG path content for use in Key panel pattern symbols.
79
- * Returns { border, content } where border is the rounded-rect outline and content
80
- * is the pattern fill. Returns null if the dataset has no pattern.
81
- *
82
- * @param {Object} dataset
83
- * @param {string} mapStyleId
84
- * @param {Object} patternRegistry
85
- * @returns {{ border: string, content: string }|null}
86
- */
87
- export const getKeyPatternPaths = (dataset, mapStyleId, patternRegistry) => {
88
- const innerContent = getPatternInnerContent(dataset, patternRegistry)
89
- if (!innerContent) {
90
- return null
91
- }
92
- const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
93
- const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
94
- const borderStroke = getValueForStyle(dataset.stroke, mapStyleId) || fg
95
- return {
96
- border: injectColors(KEY_BORDER_PATH, borderStroke, bg),
97
- content: injectColors(innerContent, fg, bg)
98
- }
99
- }
@@ -1,17 +1,9 @@
1
1
  import {
2
2
  hashString,
3
3
  injectColors,
4
- hasPattern,
5
- getPatternInnerContent,
6
- getPatternImageId,
7
- getKeyPatternPaths,
8
- KEY_BORDER_PATH
4
+ hasPattern
9
5
  } from './patternUtils.js'
10
6
 
11
- const mockRegistry = {
12
- get: (id) => id === 'dot' ? { id: 'dot', svgContent: '<path d="M4 4" fill="{{foregroundColor}}"/>' } : undefined
13
- }
14
-
15
7
  describe('hashString', () => {
16
8
  test('returns a non-empty string', () => {
17
9
  expect(typeof hashString('hello')).toBe('string')
@@ -61,117 +53,3 @@ describe('hasPattern', () => {
61
53
  expect(hasPattern({ fill: 'red' })).toBe(false)
62
54
  })
63
55
  })
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('floors effective ratio at PATTERN_MIN_PIXEL_RATIO so low-DPI ids match 2x', () => {
107
- const dataset = { fillPattern: 'dot' }
108
- const id1x = getPatternImageId(dataset, 'style-a', mockRegistry, 1)
109
- const id2x = getPatternImageId(dataset, 'style-a', mockRegistry, 2)
110
- expect(id1x).toBe(id2x)
111
- expect(id1x).toMatch(/-2x$/)
112
- })
113
-
114
- test('produces different ids for pixelRatios above the floor', () => {
115
- const dataset = { fillPattern: 'dot' }
116
- const id2x = getPatternImageId(dataset, 'style-a', mockRegistry, 2)
117
- const id3x = getPatternImageId(dataset, 'style-a', mockRegistry, 3)
118
- expect(id2x).not.toBe(id3x)
119
- expect(id2x).toMatch(/-2x$/)
120
- expect(id3x).toMatch(/-3x$/)
121
- })
122
-
123
- test('falls back to "black" foreground and "transparent" background when colours are absent', () => {
124
- const id = getPatternImageId({ fillPattern: 'dot' }, 'style-a', mockRegistry)
125
- const idExplicit = getPatternImageId(
126
- { fillPattern: 'dot', fillPatternForegroundColor: 'black', fillPatternBackgroundColor: 'transparent' },
127
- 'style-a',
128
- mockRegistry
129
- )
130
- expect(id).toBe(idExplicit)
131
- })
132
- })
133
-
134
- describe('getKeyPatternPaths', () => {
135
- test('returns border and content strings with colours injected', () => {
136
- const dataset = {
137
- fillPattern: 'dot',
138
- fillPatternForegroundColor: 'red',
139
- fillPatternBackgroundColor: 'white',
140
- stroke: 'black'
141
- }
142
- const result = getKeyPatternPaths(dataset, 'style-a', mockRegistry)
143
- expect(result).not.toBeNull()
144
- expect(result.border).toContain('black') // stroke colour
145
- expect(result.border).toContain('white') // background colour
146
- expect(result.content).toContain('red') // foreground colour
147
- expect(result.border).not.toContain('{{foregroundColor}}')
148
- expect(result.content).not.toContain('{{foregroundColor}}')
149
- })
150
-
151
- test('returns null when no pattern content is found', () => {
152
- expect(getKeyPatternPaths({ fillPattern: 'unknown' }, 'style-a', mockRegistry)).toBeNull()
153
- })
154
-
155
- test('falls back to "black" fg and "transparent" bg when colour properties are absent', () => {
156
- const result = getKeyPatternPaths({ fillPattern: 'dot' }, 'style-a', mockRegistry)
157
- expect(result).not.toBeNull()
158
- expect(result.content).toContain('black')
159
- expect(result.border).toContain('transparent')
160
- })
161
-
162
- test('border stroke falls back to foreground colour when stroke is absent', () => {
163
- const result = getKeyPatternPaths(
164
- { fillPattern: 'dot', fillPatternForegroundColor: 'green' },
165
- 'style-a',
166
- mockRegistry
167
- )
168
- expect(result).not.toBeNull()
169
- // borderStroke falls back to fg ('green'), so the border uses green for both stroke and background
170
- expect(result.border).toContain('green')
171
- })
172
-
173
- test('KEY_BORDER_PATH contains foregroundColor and backgroundColor tokens', () => {
174
- expect(KEY_BORDER_PATH).toContain('{{foregroundColor}}')
175
- expect(KEY_BORDER_PATH).toContain('{{backgroundColor}}')
176
- })
177
- })
@@ -14,23 +14,21 @@ const SYMBOL_STYLE_PROPS = new Set([
14
14
  export const hasSymbol = (dataset) => !!(dataset.symbol || dataset.symbolSvgContent)
15
15
 
16
16
  /**
17
- * Resolves the symbolDef for a dataset's symbol config.
17
+ * Returns true if a marker item should render as a standalone label with no symbol.
18
+ * Requires label content and at least one of symbol/symbolSvgContent to be explicitly null.
19
+ * Undefined values fall through to render-time default symbol resolution.
18
20
  *
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}
21
+ * @param {Object} marker
22
+ * @returns {boolean}
25
23
  */
26
- export const getSymbolDef = (dataset, symbolRegistry) => {
27
- if (dataset.symbolSvgContent) {
28
- return { svg: dataset.symbolSvgContent }
24
+ export const isStandaloneLabel = (marker) => {
25
+ if (!marker.label) {
26
+ return false
29
27
  }
30
- if (dataset.symbol) {
31
- return symbolRegistry.get(dataset.symbol)
28
+ if (marker.symbol || marker.symbolSvgContent) {
29
+ return false
32
30
  }
33
- return undefined
31
+ return marker.symbol === null || marker.symbolSvgContent === null
34
32
  }
35
33
 
36
34
  /**