@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.
Files changed (185) hide show
  1. package/assets/css/docusaurus.css +58 -34
  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/panel-definition.md +16 -0
  11. package/docs/api/symbol-config.md +160 -0
  12. package/docs/api/symbol-registry.md +115 -0
  13. package/docs/api.md +50 -23
  14. package/docs/assets/basic-map.jpg +0 -0
  15. package/docs/assets/button-first.jpg +0 -0
  16. package/docs/assets/maker-panel.jpg +0 -0
  17. package/docs/examples/add-marker-with-panel.mdx +59 -0
  18. package/docs/examples/basic-map.mdx +24 -0
  19. package/docs/examples/button-map.mdx +24 -0
  20. package/docs/examples/index.mdx +49 -0
  21. package/docs/index.mdx +1 -1
  22. package/docs/plugins/datasets.md +105 -9
  23. package/docs/plugins/interact.md +100 -44
  24. package/docs/plugins/search.md +15 -3
  25. package/docs/plugins.md +1 -1
  26. package/docusaurus.config.cjs +9 -1
  27. package/package.json +1 -1
  28. package/plugins/beta/datasets/dist/css/index.css +32 -14
  29. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  30. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  31. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  32. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  33. package/plugins/beta/datasets/src/DatasetsInit.jsx +9 -4
  34. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +57 -11
  35. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +14 -8
  36. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +155 -53
  37. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
  38. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
  39. package/plugins/beta/datasets/src/api/addDataset.js +1 -1
  40. package/plugins/beta/datasets/src/api/setData.js +4 -2
  41. package/plugins/beta/datasets/src/api/setStyle.js +2 -2
  42. package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
  43. package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
  44. package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
  45. package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
  46. package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
  47. package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
  48. package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
  49. package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
  50. package/plugins/beta/datasets/src/datasets.js +13 -4
  51. package/plugins/beta/datasets/src/defaults.js +4 -2
  52. package/plugins/beta/datasets/src/index.js +2 -1
  53. package/plugins/beta/datasets/src/manifest.js +1 -1
  54. package/plugins/beta/datasets/src/panels/Key.jsx +11 -89
  55. package/plugins/beta/datasets/src/panels/Key.module.scss +24 -13
  56. package/plugins/beta/datasets/src/panels/Layers.module.scss +13 -7
  57. package/plugins/beta/datasets/src/reducer.js +6 -0
  58. package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
  59. package/plugins/beta/datasets/src/utils/mergeSublayer.js +8 -0
  60. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  61. package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
  62. package/plugins/beta/draw-ml/dist/css/index.css +3 -0
  63. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  64. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  65. package/plugins/beta/draw-ml/dist/umd/index.js +1 -1
  66. package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
  67. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  68. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  69. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  70. package/plugins/beta/map-styles/src/MapStyles.jsx +5 -4
  71. package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
  72. package/plugins/beta/scale-bar/dist/css/index.css +1 -1
  73. package/plugins/beta/scale-bar/src/scaleBar.scss +1 -0
  74. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  75. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  76. package/plugins/interact/dist/umd/index.js +1 -1
  77. package/plugins/interact/src/InteractInit.jsx +19 -8
  78. package/plugins/interact/src/InteractInit.test.js +26 -6
  79. package/plugins/interact/src/api/clear.js +1 -1
  80. package/plugins/interact/src/api/enable.test.js +7 -7
  81. package/plugins/interact/src/api/selectMarker.js +14 -0
  82. package/plugins/interact/src/api/selectMarker.test.js +25 -0
  83. package/plugins/interact/src/api/unselectMarker.js +14 -0
  84. package/plugins/interact/src/api/unselectMarker.test.js +14 -0
  85. package/plugins/interact/src/defaults.js +4 -6
  86. package/plugins/interact/src/events.js +27 -36
  87. package/plugins/interact/src/events.test.js +119 -90
  88. package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
  89. package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
  90. package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
  91. package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
  92. package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
  93. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
  94. package/plugins/interact/src/manifest.js +10 -2
  95. package/plugins/interact/src/reducer.js +59 -5
  96. package/plugins/interact/src/reducer.test.js +100 -12
  97. package/plugins/interact/src/utils/buildStylesMap.js +17 -4
  98. package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
  99. package/plugins/interact/src/utils/featureQueries.js +11 -6
  100. package/plugins/interact/src/utils/featureQueries.test.js +8 -1
  101. package/plugins/interact/src/utils/interactionModes.js +12 -0
  102. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  103. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  104. package/plugins/search/src/Search.jsx +3 -1
  105. package/plugins/search/src/events/fetchSuggestions.js +6 -4
  106. package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
  107. package/plugins/search/src/events/formHandlers.js +3 -3
  108. package/plugins/search/src/events/formHandlers.test.js +1 -1
  109. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  110. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  111. package/plugins/search/src/utils/updateMap.js +3 -3
  112. package/plugins/search/src/utils/updateMap.test.js +3 -3
  113. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  114. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  115. package/providers/maplibre/dist/umd/index.js +1 -1
  116. package/providers/maplibre/src/appEvents.js +7 -0
  117. package/providers/maplibre/src/appEvents.test.js +18 -4
  118. package/providers/maplibre/src/maplibreProvider.js +52 -0
  119. package/providers/maplibre/src/maplibreProvider.test.js +105 -1
  120. package/providers/maplibre/src/utils/highlightFeatures.js +36 -7
  121. package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -96
  122. package/providers/maplibre/src/utils/hoverCursor.js +61 -0
  123. package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
  124. package/providers/maplibre/src/utils/patternImages.js +70 -0
  125. package/providers/maplibre/src/utils/patternImages.test.js +180 -0
  126. package/providers/maplibre/src/utils/queryFeatures.js +38 -16
  127. package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
  128. package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
  129. package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
  130. package/providers/maplibre/src/utils/symbolImages.js +147 -0
  131. package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
  132. package/src/App/components/Markers/Markers.jsx +122 -27
  133. package/src/App/components/Markers/Markers.module.scss +0 -10
  134. package/src/App/components/Markers/Markers.test.jsx +246 -0
  135. package/src/App/components/Panel/Panel.jsx +6 -6
  136. package/src/App/components/Panel/Panel.test.jsx +37 -0
  137. package/src/App/components/Viewport/Viewport.jsx +5 -15
  138. package/src/App/components/Viewport/Viewport.module.scss +2 -0
  139. package/src/App/components/Viewport/Viewport.test.jsx +16 -33
  140. package/src/App/hooks/useInterfaceAPI.js +7 -7
  141. package/src/App/hooks/useInterfaceAPI.test.js +162 -0
  142. package/src/App/hooks/useLayoutMeasurements.js +64 -72
  143. package/src/App/hooks/useMarkersAPI.js +2 -5
  144. package/src/App/hooks/useMarkersAPI.test.js +4 -4
  145. package/src/App/layout/Layout.jsx +3 -3
  146. package/src/App/layout/Layout.test.jsx +4 -2
  147. package/src/App/layout/layout.module.scss +1 -8
  148. package/src/App/renderer/HtmlElementHost.jsx +10 -5
  149. package/src/App/renderer/mapPanels.js +2 -1
  150. package/src/App/store/ServiceProvider.jsx +7 -5
  151. package/src/App/store/appActionsMap.js +4 -4
  152. package/src/App/store/appActionsMap.test.js +10 -0
  153. package/src/App/store/mapActionsMap.js +4 -6
  154. package/src/App/store/mapActionsMap.test.js +3 -2
  155. package/src/App/store/mapReducer.js +2 -1
  156. package/src/InteractiveMap/InteractiveMap.js +59 -11
  157. package/src/InteractiveMap/InteractiveMap.test.js +126 -4
  158. package/src/InteractiveMap/domStateManager.js +18 -6
  159. package/src/InteractiveMap/domStateManager.test.js +21 -0
  160. package/src/InteractiveMap/historyManager.js +28 -16
  161. package/src/InteractiveMap/historyManager.test.js +17 -0
  162. package/src/config/appConfig.js +2 -7
  163. package/src/config/appConfig.test.js +4 -15
  164. package/src/config/defaults.js +2 -3
  165. package/src/config/events.js +20 -21
  166. package/src/config/mapTheme.js +56 -0
  167. package/src/config/patternConfig.js +16 -0
  168. package/src/config/symbolConfig.js +80 -0
  169. package/src/scss/settings/_colors.scss +0 -9
  170. package/src/services/closeApp.js +1 -10
  171. package/src/services/closeApp.test.js +3 -43
  172. package/src/services/patternRegistry.js +40 -0
  173. package/src/services/patternRegistry.test.js +48 -0
  174. package/src/services/symbolRegistry.js +113 -0
  175. package/src/services/symbolRegistry.test.js +262 -0
  176. package/src/types.js +99 -12
  177. package/src/utils/mapStateSync.js +48 -10
  178. package/src/utils/mapStateSync.test.js +29 -9
  179. package/src/utils/patternUtils.js +94 -0
  180. package/src/utils/patternUtils.test.js +160 -0
  181. package/src/utils/symbolUtils.js +85 -0
  182. package/src/utils/symbolUtils.test.js +156 -0
  183. package/docs/examples.mdx +0 -70
  184. package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +0 -48
  185. package/plugins/beta/datasets/src/styles/patterns.js +0 -157
@@ -69,6 +69,27 @@ describe('updateDOMState', () => {
69
69
  }
70
70
  })
71
71
 
72
+ it('isFullscreen override forces non-fullscreen even when URL param matches', () => {
73
+ mapInstance.config.behaviour = 'buttonFirst'
74
+ queryString.getQueryParam.mockReturnValue('map') // URL says fullscreen
75
+
76
+ updateDOMState(mapInstance, { isFullscreen: false }) // but override says no
77
+
78
+ expect(document.documentElement.classList.contains('im-is-fullscreen')).toBe(false)
79
+ expect(rootEl.style.height).toBe('auto')
80
+ })
81
+
82
+ it('buttonFirst with manageHistoryState false is always fullscreen regardless of URL', () => {
83
+ mapInstance.config.behaviour = 'buttonFirst'
84
+ mapInstance.config.manageHistoryState = false
85
+ queryString.getQueryParam.mockReturnValue(null) // no mv param in URL
86
+
87
+ updateDOMState(mapInstance)
88
+
89
+ expect(document.documentElement.classList.contains('im-is-fullscreen')).toBe(true)
90
+ expect(mapInstance.rootEl.style.height).toBe('100%')
91
+ })
92
+
72
93
  describe('hybrid behaviour', () => {
73
94
  beforeEach(() => {
74
95
  mapInstance.config.behaviour = 'hybrid'
@@ -40,6 +40,32 @@ function closeMap (mapInstance) {
40
40
  }
41
41
  }
42
42
 
43
+ /**
44
+ * Syncs a single map instance against the current URL view parameter.
45
+ *
46
+ * @param {MapInstance} mapInstance
47
+ * @param {string|null} viewId - The current `mv` query param value.
48
+ * @private
49
+ */
50
+ function syncMapInstance (mapInstance, viewId) {
51
+ if (mapInstance.config.manageHistoryState === false) {
52
+ return
53
+ }
54
+
55
+ const shouldBeOpen = mapInstance.id === viewId
56
+ const isHybridVisible = mapInstance.config.behaviour === 'hybrid' && !isHybridFullscreen(mapInstance.config)
57
+ const isOpen = mapInstance.rootEl?.children.length
58
+
59
+ if (shouldBeOpen && (!isOpen || mapInstance._isHidden)) {
60
+ openMap(mapInstance)
61
+ return
62
+ }
63
+
64
+ if (!shouldBeOpen && isOpen && !isHybridVisible) {
65
+ closeMap(mapInstance)
66
+ }
67
+ }
68
+
43
69
  /**
44
70
  * Handles the `popstate` event triggered by browser back/forward navigation.
45
71
  *
@@ -56,21 +82,7 @@ function closeMap (mapInstance) {
56
82
  */
57
83
  function handlePopstate () {
58
84
  const viewId = getQueryParam(defaults.mapViewParamKey)
59
-
60
- for (const mapInstance of components.values()) {
61
- const shouldBeOpen = mapInstance.id === viewId
62
- const isHybridVisible = mapInstance.config.behaviour === 'hybrid' && !isHybridFullscreen(mapInstance.config)
63
- const isOpen = mapInstance.rootEl?.children.length
64
-
65
- if (shouldBeOpen && (!isOpen || mapInstance._isHidden)) {
66
- openMap(mapInstance)
67
- continue
68
- }
69
-
70
- if (!shouldBeOpen && isOpen && !isHybridVisible) {
71
- closeMap(mapInstance)
72
- }
73
- }
85
+ components.forEach(mapInstance => syncMapInstance(mapInstance, viewId))
74
86
  }
75
87
 
76
88
  // -----------------------------------------------------------------------------
@@ -105,7 +117,7 @@ let initialized = false
105
117
  */
106
118
  function register (component) {
107
119
  if (!initialized) {
108
- window.addEventListener('popstate', handlePopstate)
120
+ globalThis.addEventListener('popstate', handlePopstate)
109
121
  initialized = true
110
122
  }
111
123
 
@@ -141,6 +141,23 @@ describe('historyManager', () => {
141
141
  expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 768px)')
142
142
  })
143
143
 
144
+ it('skips component when manageHistoryState is false', () => {
145
+ const managedComponent = {
146
+ id: 'managed',
147
+ config: { behaviour: 'buttonFirst', hybridWidth: null, maxMobileWidth: 640, manageHistoryState: false },
148
+ rootEl: document.createElement('div'),
149
+ loadApp: jest.fn(),
150
+ removeApp: jest.fn(),
151
+ _isHidden: false
152
+ }
153
+ historyManager.register(managedComponent)
154
+ queryString.getQueryParam.mockReturnValue('managed')
155
+
156
+ window.dispatchEvent(popstateEvent)
157
+
158
+ expect(managedComponent.loadApp).not.toHaveBeenCalled()
159
+ })
160
+
144
161
  it('unregisters component', () => {
145
162
  historyManager.register(component1)
146
163
  component1.rootEl.appendChild(document.createElement('div'))
@@ -24,7 +24,7 @@ export const defaultAppConfig = {
24
24
  label: 'Exit',
25
25
  iconId: 'close',
26
26
  onClick: (_e, { services }) => services.closeApp(),
27
- excludeWhen: ({ appConfig, appState }) => !appConfig.hasExitButton || !(appState.isFullscreen && (new URL(window.location.href)).searchParams.has(appConfig.mapViewParamKey)),
27
+ excludeWhen: ({ appConfig, appState }) => !appConfig.hasExitButton || !appState.isFullscreen,
28
28
  mobile: exitButtonSlots,
29
29
  tablet: exitButtonSlots,
30
30
  desktop: exitButtonSlots
@@ -115,6 +115,7 @@ export const defaultButtonConfig = {
115
115
  // Used by addPanel
116
116
  export const defaultPanelConfig = {
117
117
  label: 'Panel',
118
+ focus: true,
118
119
  mobile: {
119
120
  slot: 'drawer',
120
121
  open: true,
@@ -159,9 +160,3 @@ export const scaleFactor = {
159
160
  medium: 1.5,
160
161
  large: 2
161
162
  }
162
-
163
- export const markerSvgPaths = [{
164
- shape: 'pin',
165
- backgroundPath: 'M31 16.001c0 7.489-8.308 15.289-11.098 17.698-.533.4-1.271.4-1.803 0C15.309 31.29 7 23.49 7 16.001c0-6.583 5.417-12 12-12s12 5.417 12 12z',
166
- graphicPath: 'M19 11.001c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.241 5-5-2.24-5-5-5z'
167
- }]
@@ -1,5 +1,5 @@
1
1
  import { render } from '@testing-library/react'
2
- import { defaultAppConfig, defaultButtonConfig, scaleFactor, markerSvgPaths } from './appConfig'
2
+ import { defaultAppConfig, defaultButtonConfig, scaleFactor } from './appConfig'
3
3
 
4
4
  describe('defaultAppConfig', () => {
5
5
  const appState = {
@@ -23,28 +23,18 @@ describe('defaultAppConfig', () => {
23
23
 
24
24
  // --- EXIT BUTTON (Line 27 Coverage) ---
25
25
  it('covers all branches of exitBtn excludeWhen', () => {
26
- const config = { hasExitButton: true, mapViewParamKey: 'view' }
27
-
28
26
  expect(exitBtn.excludeWhen({
29
- appConfig: { ...config, hasExitButton: false },
27
+ appConfig: { hasExitButton: false },
30
28
  appState: { isFullscreen: true }
31
29
  })).toBe(true)
32
30
 
33
- window.history.pushState({}, '', '?view=map')
34
31
  expect(exitBtn.excludeWhen({
35
- appConfig: config,
32
+ appConfig: { hasExitButton: true },
36
33
  appState: { isFullscreen: false }
37
34
  })).toBe(true)
38
35
 
39
- window.history.pushState({}, '', '?wrong=param')
40
- expect(exitBtn.excludeWhen({
41
- appConfig: config,
42
- appState: { isFullscreen: true }
43
- })).toBe(true)
44
-
45
- window.history.pushState({}, '', '?view=map')
46
36
  expect(exitBtn.excludeWhen({
47
- appConfig: config,
37
+ appConfig: { hasExitButton: true },
48
38
  appState: { isFullscreen: true }
49
39
  })).toBe(false)
50
40
  })
@@ -135,6 +125,5 @@ describe('defaultAppConfig', () => {
135
125
  it('exports supplementary configs and constants', () => {
136
126
  expect(defaultButtonConfig.label).toBe('Button')
137
127
  expect(scaleFactor.large).toBe(2)
138
- expect(markerSvgPaths[0].shape).toBe('pin')
139
128
  })
140
129
  })
@@ -16,7 +16,7 @@ const defaults = {
16
16
  containerHeight: '600px',
17
17
  deviceNotSupportedText: 'Your device is not supported. A map is available with a more up-to-date browser or device.',
18
18
  enableFullscreen: false,
19
- enableZoomControls: false,
19
+ enableZoomControls: true,
20
20
  genericErrorText: 'There was a problem loading the map. Please try again later.',
21
21
  hasExitButton: false,
22
22
  hybridWidth: null, // Defaults to maxMobileWidth if not set
@@ -24,11 +24,10 @@ const defaults = {
24
24
  mapLabel: 'Interactive map',
25
25
  mapProvider: null,
26
26
  mapSize: 'small',
27
+ manageHistoryState: true,
27
28
  mapViewParamKey: 'mv',
28
29
  maxMobileWidth: 640,
29
30
  minDesktopWidth: 835,
30
- markerColor: '#ff0000',
31
- markerShape: 'pin',
32
31
  nudgePanDelta: 5,
33
32
  nudgeZoomDelta: 0.1,
34
33
  panDelta: 100,
@@ -62,32 +62,34 @@ export const EVENTS = {
62
62
  APP_READY: 'app:ready',
63
63
 
64
64
  /**
65
- * Emitted when the map application becomes visible after being hidden.
65
+ * Emitted when the map application has opened and is visible.
66
66
  *
67
- * This can occur in 'hybrid behaviour' responsive scenarios where the map is already initialized
68
- * (e.g. initialized inline on desktop) but was hidden and then shown again
69
- * (e.g. resizing to mobile and opening the map).
67
+ * Fired after initial load (`loadApp`) and when the app is shown again after being hidden (`showApp`).
68
+ * Subscribe to this event to react whenever the map becomes visible to the user.
70
69
  *
71
- * @remarks
72
- * - Only emitted when transitioning from hidden → visible.
73
- * - Not fired on initial open.
74
- * - The existing map state may be preserved depending on configuration.
70
+ * Payload: `{ statePreserved: boolean }` — `true` if the map state was preserved from a previous session.
71
+ *
72
+ * @example
73
+ * map.on(EVENTS.APP_OPENED, ({ statePreserved }) => {
74
+ * console.log('Map opened, state preserved:', statePreserved)
75
+ * })
75
76
  */
76
- APP_VISIBLE: 'app:visible',
77
+ APP_OPENED: 'app:opened',
77
78
 
78
79
  /**
79
- * Emitted when the map application becomes hidden.
80
+ * Emitted when the map application has closed and is no longer visible.
81
+ *
82
+ * Fired when the app is hidden (`hideApp`) or removed (`removeApp`).
83
+ * Subscribe to this event to react whenever the map is closed.
80
84
  *
81
- * This can occur in 'hybrid behaviour' responsive scenarios where the map was initialized inline
82
- * (e.g. visible on desktop) but then becomes hidden
83
- * (e.g. resizing to mobile or closing the map view).
85
+ * Payload: `{ statePreserved: boolean }` `true` if the map state was preserved (i.e. can be restored).
84
86
  *
85
- * @remarks
86
- * - Only emitted when transitioning from visible → hidden.
87
- * - Not fired on initial load if the map starts hidden.
88
- * - The map state may be preserved depending on configuration.
87
+ * @example
88
+ * map.on(EVENTS.APP_CLOSED, ({ statePreserved }) => {
89
+ * console.log('Map closed, state preserved:', statePreserved)
90
+ * })
89
91
  */
90
- APP_HIDDEN: 'app:hidden',
92
+ APP_CLOSED: 'app:closed',
91
93
 
92
94
  /**
93
95
  * Emitted when a panel is opened.
@@ -202,9 +204,6 @@ export const EVENTS = {
202
204
  */
203
205
  MAP_CLICK: 'map:click',
204
206
 
205
- /** Emitted when the user exits the map (e.g., via the close button). */
206
- MAP_EXIT: 'map:exit',
207
-
208
207
  /** Emitted when the map is destroyed. Payload: { mapId: string } */
209
208
  MAP_DESTROY: 'map:destroy'
210
209
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Canonical colour values for light and dark map colour schemes.
3
+ * These are the single source of truth for map overlay colours —
4
+ * used both by the symbol SVG renderer (JS canvas) and injected as
5
+ * CSS custom properties onto the app container for CSS-rendered elements.
6
+ *
7
+ * Per-style overrides take precedence: set `haloColor`, `selectedColor`, or
8
+ * `foregroundColor` directly on a `MapStyleConfig` to override these defaults.
9
+ */
10
+ export const SCHEME_COLORS = {
11
+ light: {
12
+ haloColor: '#ffffff',
13
+ selectedColor: '#0b0c0c',
14
+ foregroundColor: '#0b0c0c'
15
+ },
16
+ dark: {
17
+ haloColor: '#0b0c0c',
18
+ selectedColor: '#ffffff',
19
+ foregroundColor: '#ffffff'
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Resolves the three map overlay colours for the given map style,
25
+ * falling back to the appropriate scheme defaults when not explicitly set.
26
+ * Stored in `mapState.mapTheme` so plugins can read resolved values without
27
+ * importing from core.
28
+ *
29
+ * @param {import('../types.js').MapStyleConfig|null} mapStyle
30
+ * @returns {{ haloColor: string, selectedColor: string, foregroundColor: string }}
31
+ */
32
+ export const resolveMapTheme = (mapStyle) => {
33
+ const scheme = SCHEME_COLORS[mapStyle?.mapColorScheme] ?? SCHEME_COLORS.light
34
+ return {
35
+ haloColor: mapStyle?.haloColor ?? scheme.haloColor,
36
+ selectedColor: mapStyle?.selectedColor ?? scheme.selectedColor,
37
+ foregroundColor: mapStyle?.foregroundColor ?? scheme.foregroundColor
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Converts a mapStyle's overlay colours into CSS custom properties,
43
+ * falling back to the appropriate scheme defaults when not explicitly set.
44
+ * Suitable for spreading directly into a React `style` prop.
45
+ *
46
+ * @param {import('../types.js').MapStyleConfig|null} mapStyle
47
+ * @returns {Object} CSS custom property object
48
+ */
49
+ export const getMapThemeVars = (mapStyle) => {
50
+ const { haloColor, selectedColor, foregroundColor } = resolveMapTheme(mapStyle)
51
+ return {
52
+ '--map-overlay-halo-color': haloColor,
53
+ '--map-overlay-selected-color': selectedColor,
54
+ '--map-overlay-foreground-color': foregroundColor
55
+ }
56
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Built-in fill pattern SVG content.
3
+ * Each value is the inner SVG paths only (no wrapper element).
4
+ * Paths are authored in a 16×16 coordinate space and tile seamlessly.
5
+ * Use {{foregroundColor}} and {{backgroundColor}} tokens for colour injection.
6
+ */
7
+ export const BUILT_IN_PATTERNS = {
8
+ 'cross-hatch': '<path d="M0 4.486V3.485h3.5V.001h1v3.484h7.002V.001h1v3.484h3.5v1.001h-3.5v7h3.5v.999h-3.5v3.516h-1v-3.516H4.499v3.516h-1v-3.516H0v-.999h3.5v-7H0zm11.501 0H4.499v7h7.002v-7z" fill="{{foregroundColor}}"/>',
9
+ 'diagonal-cross-hatch': '<path d="M0 8.707V7.293L7.293 0h1.414L16 7.293v1.414L8.707 16H7.293L0 8.707zM.707 8L8 15.293 15.293 8 8 .707.707 8z" fill="{{foregroundColor}}"/>',
10
+ 'forward-diagonal-hatch': '<path d="M16 8.707V7.293L7.293 16h1.414L16 8.707zm-16 0L8.707 0H7.293L0 7.293v1.414z" fill="{{foregroundColor}}"/>',
11
+ 'backward-diagonal-hatch': '<path d="M0 8.707V7.293L8.707 16H7.293L0 8.707zm16 0L7.293 0h1.414L16 7.293v1.414z" fill="{{foregroundColor}}"/>',
12
+ 'horizontal-hatch': '<path d="M0 4.5V3.499h15.999V4.5H0zm0 7h15.999V12.5H0v-1.001z" fill="{{foregroundColor}}"/>',
13
+ 'vertical-hatch': '<path d="M3.501 16.001V0h1v16.001h-1zm7.998 0V0h1v16.001h-1z" fill="{{foregroundColor}}"/>',
14
+ dot: '<path d="M3.999 2A2 2 0 0 1 6 3.999C6 5.103 5.103 6 3.999 6a2 2 0 0 1-1.999-2.001A2 2 0 0 1 3.999 2zm0 7.999C5.103 10 6 10.897 6 12.001A2 2 0 0 1 3.999 14a2 2 0 0 1-1.999-1.999A2 2 0 0 1 3.999 10zM11.999 2A2 2 0 0 1 14 3.999C14 5.103 13.103 6 11.999 6S10 5.103 10 3.999A2 2 0 0 1 11.999 2zm0 7.999c1.104 0 2.001.897 2.001 2.001A2 2 0 0 1 11.999 14 2 2 0 0 1 10 12.001c0-1.104.897-2.001 1.999-2.001z" fill="{{foregroundColor}}"/>',
15
+ diamond: '<path d="M4 .465L7.535 4 4 7.535.465 4 4 .465zm0 7.999l3.535 3.535L4 15.535.465 11.999 4 8.464zm8-8l3.535 3.535-3.536 3.536L8.464 4 12 .464zm0 8.001L15.536 12 12 15.536 8.465 12 12 8.465z" fill="{{foregroundColor}}"/>'
16
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Default token values applied to all symbols unless overridden at the constructor,
3
+ * symbol registration, or marker creation level.
4
+ * Colour values may be a plain string or a map-style-keyed object,
5
+ * e.g. { outdoor: '#ffffff', dark: '#0b0c0c' }
6
+ */
7
+ export const symbolDefaults = {
8
+ symbol: 'pin',
9
+ backgroundColor: '#ca3535',
10
+ foregroundColor: '#ffffff',
11
+ haloWidth: '1',
12
+ selectedWidth: '6'
13
+ }
14
+
15
+ /**
16
+ * Built-in graphic path data strings for use with the `graphic` token.
17
+ *
18
+ * Each value is an SVG `d` attribute string in a 16×16 coordinate space,
19
+ * centred at (8, 8). The built-in symbols (`pin`, `circle`, `square`) apply a
20
+ * `translate` transform to position this 16×16 area correctly within their
21
+ * 38×38 viewBox — so graphic path data does not need to account for symbol
22
+ * positioning.
23
+ *
24
+ * @example
25
+ * markers.add('id', coords, { symbol: 'pin', graphic: graphics.dot })
26
+ *
27
+ * @example
28
+ * // Inline path data (16×16 space, centred at 8,8)
29
+ * markers.add('id', coords, { symbol: 'pin', graphic: 'M3 8 L8 3 L13 8 L8 13 Z' })
30
+ */
31
+ export const graphics = {
32
+ /** Small filled circle — the default graphic for built-in symbols */
33
+ dot: 'M8 3c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.241 5-5-2.24-5-5-5z',
34
+
35
+ /** Filled plus / cross shape */
36
+ cross: 'M6 3H10V6H13V10H10V13H6V10H3V6H6Z',
37
+
38
+ /** Filled diamond / rotated square */
39
+ diamond: 'M8 2L14 8L8 14L2 8Z',
40
+
41
+ /** Filled upward-pointing triangle */
42
+ triangle: 'M8 2L14 14H2Z',
43
+
44
+ /** Filled square */
45
+ square: 'M3 3H13V13H3Z'
46
+ }
47
+
48
+ // ─── Built-in symbol definitions ─────────────────────────────────────────────
49
+ // Each symbol uses a 38×38 viewBox. SVG templates use {{token}} placeholders
50
+ // resolved at render time by the symbolRegistry.
51
+
52
+ export const pin = {
53
+ id: 'pin',
54
+ viewBox: '0 0 38 38',
55
+ anchor: [0.5, 0.9], // NOSONAR
56
+ graphic: graphics.dot,
57
+ svg: `<path d="M19 33.499c-5.318-5-12-9.509-12-16.998 0-6.583 5.417-12 12-12s12 5.417 12 12c0 7.489-6.682 11.998-12 16.998z" fill="none" stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/>
58
+ <path d="M19 33.499c-5.318-5-12-9.509-12-16.998 0-6.583 5.417-12 12-12s12 5.417 12 12c0 7.489-6.682 11.998-12 16.998z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/>
59
+ <g transform="translate(19, 16) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
60
+ }
61
+
62
+ export const circle = {
63
+ id: 'circle',
64
+ viewBox: '0 0 38 38',
65
+ anchor: [0.5, 0.5],
66
+ graphic: graphics.dot,
67
+ svg: `<path d="M19 7C12.376 7 7 12.376 7 19s5.376 12 12 12a12.01 12.01 0 0 0 12-12A12.01 12.01 0 0 0 19 7z" fill="none" stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/>
68
+ <path d="M19 7C12.376 7 7 12.376 7 19s5.376 12 12 12a12.01 12.01 0 0 0 12-12A12.01 12.01 0 0 0 19 7z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/>
69
+ <g transform="translate(19, 19) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
70
+ }
71
+
72
+ export const square = {
73
+ id: 'square',
74
+ viewBox: '0 0 38 38',
75
+ anchor: [0.5, 0.5],
76
+ graphic: graphics.dot,
77
+ svg: `<path d="M28 7a3 3 0 0 1 3 3v18a3 3 0 0 1-3 3H10a3 3 0 0 1-3-3V10a3 3 0 0 1 3-3h18z" fill="none" stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/>
78
+ <path d="M28 7a3 3 0 0 1 3 3v18a3 3 0 0 1-3 3H10a3 3 0 0 1-3-3V10a3 3 0 0 1 3-3h18z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/>
79
+ <g transform="translate(19, 19) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
80
+ }
@@ -35,10 +35,6 @@
35
35
  --attributions-foreground-color: #0b0c0c;
36
36
  --attributions-background-color: #ffffff80;
37
37
 
38
- // Map overlays, such as scale bar, target marker etc
39
- --map-overlay-foreground-color: #0b0c0c;
40
- --map-overlay-halo-color: #ffffff;
41
-
42
38
  // Fixed colours (Dont chnage with dark mode)
43
39
  --fixed-foreground-color: #0b0c0c;
44
40
 
@@ -78,8 +74,3 @@
78
74
  --content-link-color: #ffffff;
79
75
  }
80
76
 
81
- // Map color scheme specific overrides
82
- :root .im-o-app--dark-map {
83
- --map-overlay-foreground-color: #ffffff;
84
- --map-overlay-halo-color: #0b0c0c;
85
- }
@@ -1,13 +1,4 @@
1
1
  // src/services/closeApp.js
2
- import { EVENTS as events } from '../config/events.js'
3
-
4
- export function closeApp (mapId, handleExitClick, eventBus) {
5
- eventBus.emit(events.MAP_EXIT, { mapId })
6
-
7
- if (history.state?.isBack) {
8
- history.back()
9
- return
10
- }
11
-
2
+ export function closeApp (_mapId, handleExitClick) {
12
3
  handleExitClick()
13
4
  }
@@ -1,49 +1,9 @@
1
1
  import { closeApp } from './closeApp'
2
2
 
3
3
  describe('closeApp', () => {
4
- let handleExitClickMock
5
- let mockEventBus
6
-
7
- beforeEach(() => {
8
- handleExitClickMock = jest.fn()
9
- mockEventBus = {
10
- emit: jest.fn(),
11
- on: jest.fn(),
12
- off: jest.fn()
13
- }
14
- jest.spyOn(history, 'back').mockImplementation(() => {})
15
- })
16
-
17
- afterEach(() => {
18
- jest.restoreAllMocks()
19
- jest.clearAllMocks()
20
- })
21
-
22
- it('calls history.back() when history.state.isBack is true', () => {
23
- Object.defineProperty(history, 'state', {
24
- value: { isBack: true },
25
- writable: true,
26
- configurable: true
27
- })
28
-
29
- closeApp('map-123', handleExitClickMock, mockEventBus)
30
-
31
- expect(mockEventBus.emit).toHaveBeenCalledWith('map:exit', { mapId: 'map-123' })
32
- expect(history.back).toHaveBeenCalled()
33
- expect(handleExitClickMock).not.toHaveBeenCalled()
34
- })
35
-
36
- it('calls handleExitClick when history.state.isBack is not true', () => {
37
- Object.defineProperty(history, 'state', {
38
- value: null,
39
- writable: true,
40
- configurable: true
41
- })
42
-
43
- closeApp('map-123', handleExitClickMock, mockEventBus)
44
-
45
- expect(mockEventBus.emit).toHaveBeenCalledWith('map:exit', { mapId: 'map-123' })
46
- expect(history.back).not.toHaveBeenCalled()
4
+ it('calls handleExitClick', () => {
5
+ const handleExitClickMock = jest.fn()
6
+ closeApp('map-123', handleExitClickMock)
47
7
  expect(handleExitClickMock).toHaveBeenCalled()
48
8
  })
49
9
  })
@@ -0,0 +1,40 @@
1
+ import { BUILT_IN_PATTERNS } from '../config/patternConfig.js'
2
+
3
+ const patterns = new Map()
4
+
5
+ export const patternRegistry = {
6
+ /**
7
+ * Register a named pattern.
8
+ *
9
+ * @param {string} id - Unique pattern name (e.g. 'my-hatch')
10
+ * @param {string} svgContent - Inner SVG path content in a 16×16 coordinate space.
11
+ * Use {{foregroundColor}} and {{backgroundColor}} tokens for colour injection.
12
+ */
13
+ register (id, svgContent) {
14
+ patterns.set(id, { id, svgContent })
15
+ },
16
+
17
+ /**
18
+ * Retrieve a registered pattern by name.
19
+ *
20
+ * @param {string} id
21
+ * @returns {{ id: string, svgContent: string }|undefined}
22
+ */
23
+ get (id) {
24
+ return patterns.get(id)
25
+ },
26
+
27
+ /**
28
+ * Returns all registered patterns.
29
+ *
30
+ * @returns {{ id: string, svgContent: string }[]}
31
+ */
32
+ list () {
33
+ return [...patterns.values()]
34
+ }
35
+ }
36
+
37
+ // Seed built-in patterns
38
+ Object.entries(BUILT_IN_PATTERNS).forEach(([id, svgContent]) => {
39
+ patternRegistry.register(id, svgContent)
40
+ })
@@ -0,0 +1,48 @@
1
+ import { patternRegistry } from './patternRegistry.js'
2
+
3
+ describe('patternRegistry', () => {
4
+ describe('built-in patterns', () => {
5
+ test.each([
6
+ 'cross-hatch',
7
+ 'diagonal-cross-hatch',
8
+ 'forward-diagonal-hatch',
9
+ 'backward-diagonal-hatch',
10
+ 'horizontal-hatch',
11
+ 'vertical-hatch',
12
+ 'dot',
13
+ 'diamond'
14
+ ])('seeds built-in pattern: %s', (id) => {
15
+ const pattern = patternRegistry.get(id)
16
+ expect(pattern).toBeDefined()
17
+ expect(pattern.id).toBe(id)
18
+ expect(typeof pattern.svgContent).toBe('string')
19
+ expect(pattern.svgContent.length).toBeGreaterThan(0)
20
+ })
21
+ })
22
+
23
+ describe('register / get', () => {
24
+ test('registers a custom pattern and retrieves it by id', () => {
25
+ patternRegistry.register('test-hatch', '<path d="M0 0L8 8" stroke="{{foreground}}"/>')
26
+ const result = patternRegistry.get('test-hatch')
27
+ expect(result).toEqual({ id: 'test-hatch', svgContent: '<path d="M0 0L8 8" stroke="{{foreground}}"/>' })
28
+ })
29
+
30
+ test('returns undefined for an unregistered id', () => {
31
+ expect(patternRegistry.get('nonexistent-pattern')).toBeUndefined()
32
+ })
33
+
34
+ test('overwrite an existing pattern by re-registering the same id', () => {
35
+ patternRegistry.register('overwrite-test', '<path d="M0 0"/>')
36
+ patternRegistry.register('overwrite-test', '<path d="M1 1"/>')
37
+ expect(patternRegistry.get('overwrite-test').svgContent).toBe('<path d="M1 1"/>')
38
+ })
39
+ })
40
+
41
+ describe('list', () => {
42
+ test('returns all registered patterns including built-ins', () => {
43
+ const all = patternRegistry.list()
44
+ expect(all.length).toBeGreaterThanOrEqual(8)
45
+ expect(all.every(p => p.id && p.svgContent)).toBe(true)
46
+ })
47
+ })
48
+ })