@defra/interactive-map 0.0.15-alpha → 0.0.17-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 (263) hide show
  1. package/assets/css/docusaurus.css +104 -0
  2. package/assets/images/favicon.svg +1 -0
  3. package/assets/images/hero.png +0 -0
  4. package/assets/images/slot-map.svg +264 -0
  5. package/dist/css/index.css +1 -1
  6. package/dist/esm/im-core.js +1 -1
  7. package/dist/esm/im-shell.js +1 -1
  8. package/dist/umd/im-core.js +1 -1
  9. package/dist/umd/index.js +1 -1
  10. package/docs/api/slots.md +90 -6
  11. package/docs/api.md +4 -4
  12. package/docs/architecture.md +3 -1
  13. package/docs/{demo.mdx → examples.mdx} +1 -1
  14. package/docs/getting-started.md +5 -4
  15. package/docs/index.mdx +42 -0
  16. package/docs/plugins/datasets.md +561 -0
  17. package/docs/plugins/interact.md +176 -55
  18. package/docs/plugins/map-styles.md +64 -7
  19. package/docs/plugins/search.md +207 -63
  20. package/docs/plugins.md +8 -16
  21. package/docusaurus.config.cjs +34 -34
  22. package/jest.setup.js +1 -1
  23. package/package.json +6 -5
  24. package/plugins/beta/datasets/dist/css/index.css +85 -15
  25. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  26. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  27. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  28. package/plugins/beta/datasets/src/DatasetsInit.jsx +24 -9
  29. package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
  30. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +113 -0
  31. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +69 -0
  32. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +338 -0
  33. package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +48 -0
  34. package/plugins/beta/datasets/src/api/addDataset.js +3 -9
  35. package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
  36. package/plugins/beta/datasets/src/api/getStyle.js +13 -0
  37. package/plugins/beta/datasets/src/api/removeDataset.js +3 -45
  38. package/plugins/beta/datasets/src/api/setData.js +8 -0
  39. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
  40. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
  41. package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
  42. package/plugins/beta/datasets/src/api/setStyle.js +22 -0
  43. package/plugins/beta/datasets/src/datasets.js +33 -59
  44. package/plugins/beta/datasets/src/defaults.js +43 -9
  45. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +39 -30
  46. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
  47. package/plugins/beta/datasets/src/manifest.js +27 -19
  48. package/plugins/beta/datasets/src/panels/Key.jsx +129 -49
  49. package/plugins/beta/datasets/src/panels/Key.module.scss +48 -9
  50. package/plugins/beta/datasets/src/panels/Layers.jsx +131 -29
  51. package/plugins/beta/datasets/src/panels/Layers.module.scss +50 -8
  52. package/plugins/beta/datasets/src/reducer.js +128 -9
  53. package/plugins/beta/datasets/src/styles/patterns.js +157 -0
  54. package/plugins/beta/datasets/src/utils/bbox.js +8 -6
  55. package/plugins/beta/datasets/src/utils/filters.js +5 -2
  56. package/plugins/beta/datasets/src/utils/mergeSublayer.js +78 -0
  57. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  58. package/plugins/beta/draw-es/src/DrawInit.jsx +16 -16
  59. package/plugins/beta/draw-es/src/api/addFeature.js +3 -3
  60. package/plugins/beta/draw-es/src/api/deleteFeature.js +3 -3
  61. package/plugins/beta/draw-es/src/api/editFeature.js +3 -3
  62. package/plugins/beta/draw-es/src/api/newPolygon.js +3 -3
  63. package/plugins/beta/draw-es/src/events.js +52 -20
  64. package/plugins/beta/draw-es/src/events.test.js +301 -0
  65. package/plugins/beta/draw-es/src/graphic.js +1 -1
  66. package/plugins/beta/draw-es/src/manifest.js +4 -4
  67. package/plugins/beta/draw-es/src/reducer.js +1 -1
  68. package/plugins/beta/draw-es/src/sketchViewModel.js +1 -1
  69. package/plugins/beta/draw-ml/dist/css/index.css +1 -1
  70. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  71. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  72. package/plugins/beta/draw-ml/src/DrawInit.jsx +49 -52
  73. package/plugins/beta/draw-ml/src/api/deleteFeature.js +1 -1
  74. package/plugins/beta/draw-ml/src/api/editFeature.js +8 -5
  75. package/plugins/beta/draw-ml/src/api/newLine.js +0 -1
  76. package/plugins/beta/draw-ml/src/api/newPolygon.js +0 -1
  77. package/plugins/beta/draw-ml/src/api/split.js +4 -4
  78. package/plugins/beta/draw-ml/src/defaults.js +1 -1
  79. package/plugins/beta/draw-ml/src/draw.scss +0 -7
  80. package/plugins/beta/draw-ml/src/events.js +8 -6
  81. package/plugins/beta/draw-ml/src/manifest.js +29 -29
  82. package/plugins/beta/draw-ml/src/mapboxDraw.js +1 -1
  83. package/plugins/beta/draw-ml/src/mapboxSnap.js +17 -18
  84. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -31
  85. package/plugins/beta/draw-ml/src/modes/disabledMode.js +1 -1
  86. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +11 -11
  87. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +7 -7
  88. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +8 -8
  89. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +7 -7
  90. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +32 -24
  91. package/plugins/beta/draw-ml/src/reducer.js +1 -1
  92. package/plugins/beta/draw-ml/src/undoStack.js +4 -4
  93. package/plugins/beta/draw-ml/src/utils/snapHelpers.js +12 -12
  94. package/plugins/beta/draw-ml/src/utils/spatial.js +11 -11
  95. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  96. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  97. package/plugins/beta/frame/src/Frame.jsx +9 -9
  98. package/plugins/beta/frame/src/FrameInit.jsx +4 -4
  99. package/plugins/beta/frame/src/api/addFrame.js +1 -1
  100. package/plugins/beta/frame/src/api/editFeature.js +1 -1
  101. package/plugins/beta/frame/src/config.js +1 -1
  102. package/plugins/beta/frame/src/manifest.js +3 -3
  103. package/plugins/beta/frame/src/reducer.js +1 -1
  104. package/plugins/beta/frame/src/utils.js +1 -1
  105. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  106. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  107. package/plugins/beta/map-styles/src/MapStyles.jsx +18 -18
  108. package/plugins/beta/map-styles/src/manifest.js +1 -1
  109. package/plugins/beta/scale-bar/dist/css/index.css +1 -1
  110. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
  111. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
  112. package/plugins/beta/scale-bar/src/ScaleBar.jsx +5 -5
  113. package/plugins/beta/scale-bar/src/index.test.js +3 -3
  114. package/plugins/beta/scale-bar/src/manifest.js +3 -3
  115. package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
  116. package/plugins/beta/use-location/src/UseLocation.jsx +1 -1
  117. package/plugins/beta/use-location/src/defaults.js +1 -1
  118. package/plugins/beta/use-location/src/events.js +3 -3
  119. package/plugins/interact/dist/css/index.css +1 -1
  120. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  121. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  122. package/plugins/interact/src/InteractInit.jsx +1 -2
  123. package/plugins/interact/src/api/enable.js +8 -5
  124. package/plugins/interact/src/api/enable.test.js +2 -2
  125. package/plugins/interact/src/api/selectFeature.js +4 -4
  126. package/plugins/interact/src/api/unselectFeature.js +5 -5
  127. package/plugins/interact/src/defaults.js +0 -1
  128. package/plugins/interact/src/events.test.js +15 -15
  129. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  130. package/plugins/interact/src/hooks/useInteractionHandlers.js +2 -2
  131. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -5
  132. package/plugins/interact/src/interact.scss +0 -7
  133. package/plugins/interact/src/manifest.js +15 -19
  134. package/plugins/interact/src/manifest.test.js +6 -5
  135. package/plugins/interact/src/reducer.js +3 -3
  136. package/plugins/interact/src/reducer.test.js +0 -1
  137. package/plugins/interact/src/utils/spatial.js +10 -10
  138. package/plugins/interact/src/utils/spatial.test.js +14 -14
  139. package/plugins/search/dist/css/index.css +1 -1
  140. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  141. package/plugins/search/dist/esm/index.js +1 -1
  142. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  143. package/plugins/search/dist/umd/index.js +1 -1
  144. package/plugins/search/src/Search.jsx +7 -6
  145. package/plugins/search/src/Search.test.jsx +23 -23
  146. package/plugins/search/src/components/CloseButton/CloseButton.jsx +15 -15
  147. package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +2 -2
  148. package/plugins/search/src/components/Form/Form.jsx +14 -14
  149. package/plugins/search/src/components/Form/Form.module.scss +2 -1
  150. package/plugins/search/src/components/Form/Form.test.jsx +11 -11
  151. package/plugins/search/src/components/OpenButton/OpenButton.jsx +16 -15
  152. package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +6 -2
  153. package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +15 -15
  154. package/plugins/search/src/components/Suggestions/Suggestions.jsx +6 -6
  155. package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +4 -4
  156. package/plugins/search/src/datasets.js +12 -13
  157. package/plugins/search/src/datasets.test.js +1 -1
  158. package/plugins/search/src/defaults.js +1 -1
  159. package/plugins/search/src/events/fetchSuggestions.js +3 -3
  160. package/plugins/search/src/events/fetchSuggestions.test.js +1 -1
  161. package/plugins/search/src/events/formHandlers.js +3 -3
  162. package/plugins/search/src/events/formHandlers.test.js +1 -1
  163. package/plugins/search/src/events/index.js +2 -2
  164. package/plugins/search/src/events/index.test.js +2 -2
  165. package/plugins/search/src/events/inputHandlers.js +4 -4
  166. package/plugins/search/src/events/inputHandlers.test.js +1 -1
  167. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  168. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  169. package/plugins/search/src/index.js +2 -1
  170. package/plugins/search/src/index.test.js +3 -3
  171. package/plugins/search/src/manifest.js +6 -4
  172. package/plugins/search/src/reducer.js +1 -2
  173. package/plugins/search/src/reducer.test.js +2 -2
  174. package/plugins/search/src/search.scss +10 -3
  175. package/plugins/search/src/utils/parseOsNamesResults.js +1 -2
  176. package/plugins/search/src/utils/parseOsNamesResults.test.js +2 -2
  177. package/plugins/search/src/utils/updateMap.js +1 -1
  178. package/plugins/search/src/utils/updateMap.test.js +5 -5
  179. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  180. package/providers/beta/esri/src/esriProvider.js +5 -5
  181. package/providers/beta/esri/src/utils/coords.js +1 -1
  182. package/providers/beta/esri/src/utils/esriFixes.js +1 -1
  183. package/providers/beta/esri/src/utils/query.js +4 -4
  184. package/providers/beta/esri/src/utils/spatial.js +1 -2
  185. package/providers/beta/esri/src/utils/spatial.test.js +4 -1
  186. package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +1 -1
  187. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  188. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  189. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  190. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  191. package/providers/maplibre/src/appEvents.test.js +1 -1
  192. package/providers/maplibre/src/index.js +1 -1
  193. package/providers/maplibre/src/index.test.js +3 -5
  194. package/providers/maplibre/src/mapEvents.test.js +15 -5
  195. package/providers/maplibre/src/maplibreProvider.test.js +6 -2
  196. package/providers/maplibre/src/utils/calculateLinearTextSize.js +4 -4
  197. package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +3 -3
  198. package/providers/maplibre/src/utils/detectWebgl.test.js +1 -1
  199. package/providers/maplibre/src/utils/highlightFeatures.js +3 -2
  200. package/providers/maplibre/src/utils/highlightFeatures.test.js +13 -6
  201. package/providers/maplibre/src/utils/labels.js +19 -20
  202. package/providers/maplibre/src/utils/labels.test.js +15 -13
  203. package/providers/maplibre/src/utils/maplibreFixes.test.js +1 -1
  204. package/providers/maplibre/src/utils/queryFeatures.js +6 -6
  205. package/providers/maplibre/src/utils/queryFeatures.test.js +13 -13
  206. package/providers/maplibre/src/utils/spatial.js +0 -1
  207. package/providers/maplibre/src/utils/spatial.test.js +26 -27
  208. package/src/App/components/Actions/Actions.jsx +2 -2
  209. package/src/App/components/Actions/Actions.module.scss +0 -7
  210. package/src/App/components/Actions/Actions.test.jsx +1 -1
  211. package/src/App/components/Icon/Icon.jsx +3 -2
  212. package/src/App/components/Icon/Icon.module.scss +4 -0
  213. package/src/App/components/Icon/Icon.test.jsx +43 -4
  214. package/src/App/components/MapButton/MapButton.jsx +42 -17
  215. package/src/App/components/MapButton/MapButton.module.scss +4 -13
  216. package/src/App/components/MapButton/MapButton.test.jsx +27 -3
  217. package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
  218. package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
  219. package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
  220. package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
  221. package/src/App/hooks/useButtonStateEvaluator.js +12 -2
  222. package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
  223. package/src/App/hooks/useInterfaceAPI.js +6 -0
  224. package/src/App/hooks/useLayoutMeasurements.js +84 -18
  225. package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
  226. package/src/App/layout/Layout.jsx +12 -7
  227. package/src/App/layout/Layout.test.jsx +2 -2
  228. package/src/App/layout/layout.module.scss +67 -29
  229. package/src/App/registry/pluginRegistry.js +17 -0
  230. package/src/App/registry/pluginRegistry.test.js +33 -0
  231. package/src/App/renderer/HtmlElementHost.jsx +2 -1
  232. package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
  233. package/src/App/renderer/mapButtons.js +3 -2
  234. package/src/App/renderer/mapPanels.test.js +2 -2
  235. package/src/App/renderer/slotHelpers.js +2 -2
  236. package/src/App/renderer/slotHelpers.test.js +5 -5
  237. package/src/App/renderer/slots.js +9 -5
  238. package/src/App/store/AppProvider.jsx +3 -1
  239. package/src/App/store/AppProvider.test.jsx +1 -1
  240. package/src/App/store/ServiceProvider.jsx +3 -1
  241. package/src/App/store/appActionsMap.js +16 -0
  242. package/src/App/store/appActionsMap.test.js +27 -0
  243. package/src/App/store/appDispatchMiddleware.js +33 -1
  244. package/src/App/store/appDispatchMiddleware.test.js +250 -222
  245. package/src/App/store/appReducer.js +2 -0
  246. package/src/InteractiveMap/InteractiveMap.js +4 -0
  247. package/src/config/appConfig.js +7 -4
  248. package/src/config/events.js +28 -0
  249. package/src/scss/main.scss +1 -0
  250. package/src/scss/settings/_dimensions.scss +0 -1
  251. package/src/services/logger.js +6 -0
  252. package/src/services/logger.test.js +32 -0
  253. package/src/utils/getSafeZoneInset.js +9 -7
  254. package/src/utils/getSafeZoneInset.test.js +10 -10
  255. package/webpack.dev.mjs +23 -19
  256. package/docs/govuk-prototype.md +0 -23
  257. package/docs/index.md +0 -19
  258. package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
  259. package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
  260. package/plugins/beta/datasets/src/api/showDataset.js +0 -14
  261. package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
  262. package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
  263. package/plugins/beta/datasets/src/mapLayers.js +0 -165
@@ -0,0 +1,37 @@
1
+ export const setDatasetVisibility = ({ pluginState }, visible, options = {}) => {
2
+ const { datasetId, sublayerId } = options
3
+
4
+ if (sublayerId) {
5
+ const visibility = visible ? 'visible' : 'hidden'
6
+ pluginState.layerAdapter?.[visible ? 'showSublayer' : 'hideSublayer'](datasetId, sublayerId)
7
+ pluginState.dispatch({ type: 'SET_SUBLAYER_VISIBILITY', payload: { datasetId, sublayerId, visibility } })
8
+ return
9
+ }
10
+
11
+ if (datasetId) {
12
+ pluginState.layerAdapter?.[visible ? 'showDataset' : 'hideDataset'](datasetId)
13
+ pluginState.dispatch({ type: 'SET_DATASET_VISIBILITY', payload: { id: datasetId, visibility: visible ? 'visible' : 'hidden' } })
14
+ if (visible) {
15
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
16
+ Object.entries(dataset?.sublayerVisibility || {}).forEach(([subId, vis]) => {
17
+ if (vis === 'hidden') {
18
+ pluginState.layerAdapter?.hideSublayer(datasetId, subId)
19
+ }
20
+ })
21
+ }
22
+ return
23
+ }
24
+
25
+ // Global
26
+ pluginState.dispatch({ type: 'SET_GLOBAL_VISIBILITY', payload: { visibility: visible ? 'visible' : 'hidden' } })
27
+ pluginState.datasets?.forEach(dataset => {
28
+ pluginState.layerAdapter?.[visible ? 'showDataset' : 'hideDataset'](dataset.id)
29
+ if (visible) {
30
+ Object.entries(dataset.sublayerVisibility || {}).forEach(([subId, vis]) => {
31
+ if (vis === 'hidden') {
32
+ pluginState.layerAdapter?.hideSublayer(dataset.id, subId)
33
+ }
34
+ })
35
+ }
36
+ })
37
+ }
@@ -0,0 +1,22 @@
1
+ export const setFeatureVisibility = ({ pluginState }, visible, featureIds, { datasetId, idProperty = null } = {}) => {
2
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
3
+ if (!dataset) {
4
+ return
5
+ }
6
+
7
+ if (visible) {
8
+ const existingHidden = pluginState.hiddenFeatures[datasetId]
9
+ if (!existingHidden) {
10
+ return
11
+ }
12
+ const remainingHiddenIds = existingHidden.ids.filter(id => !featureIds.includes(id))
13
+ pluginState.dispatch({ type: 'SHOW_FEATURES', payload: { layerId: datasetId, featureIds } })
14
+ pluginState.layerAdapter?.showFeatures(dataset, idProperty, remainingHiddenIds)
15
+ } else {
16
+ const existingHidden = pluginState.hiddenFeatures[datasetId]
17
+ const existingIds = existingHidden?.ids || []
18
+ const allHiddenIds = [...new Set([...existingIds, ...featureIds])]
19
+ pluginState.dispatch({ type: 'HIDE_FEATURES', payload: { layerId: datasetId, idProperty, featureIds } })
20
+ pluginState.layerAdapter?.hideFeatures(dataset, idProperty, allHiddenIds)
21
+ }
22
+ }
@@ -0,0 +1,29 @@
1
+ export const setOpacity = ({ pluginState }, opacity, options) => {
2
+ const { datasetId, sublayerId } = options || {}
3
+
4
+ if (sublayerId) {
5
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
6
+ if (!dataset) {
7
+ return
8
+ }
9
+ pluginState.dispatch({ type: 'SET_SUBLAYER_OPACITY', payload: { datasetId, sublayerId, opacity } })
10
+ pluginState.layerAdapter?.setSublayerOpacity(datasetId, sublayerId, opacity)
11
+ return
12
+ }
13
+
14
+ if (datasetId) {
15
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
16
+ if (!dataset) {
17
+ return
18
+ }
19
+ pluginState.dispatch({ type: 'SET_OPACITY', payload: { datasetId, opacity } })
20
+ pluginState.layerAdapter?.setOpacity(datasetId, opacity)
21
+ return
22
+ }
23
+
24
+ // Global
25
+ pluginState.dispatch({ type: 'SET_GLOBAL_OPACITY', payload: { opacity } })
26
+ pluginState.datasets?.forEach(d => {
27
+ pluginState.layerAdapter?.setOpacity(d.id, opacity)
28
+ })
29
+ }
@@ -0,0 +1,22 @@
1
+ export const setStyle = ({ pluginState, mapState }, style, { datasetId, sublayerId } = {}) => {
2
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
3
+ if (!dataset) {
4
+ return
5
+ }
6
+
7
+ if (sublayerId) {
8
+ pluginState.dispatch({ type: 'SET_SUBLAYER_STYLE', payload: { datasetId, sublayerId, styleChanges: style } })
9
+ const updatedSublayerDataset = {
10
+ ...dataset,
11
+ sublayers: dataset.sublayers?.map(sublayer =>
12
+ sublayer.id === sublayerId ? { ...sublayer, style: { ...sublayer.style, ...style } } : sublayer
13
+ )
14
+ }
15
+ pluginState.layerAdapter?.setSublayerStyle(updatedSublayerDataset, sublayerId, mapState.mapStyle.id)
16
+ return
17
+ }
18
+
19
+ pluginState.dispatch({ type: 'SET_DATASET_STYLE', payload: { datasetId, styleChanges: style } })
20
+ const updatedDataset = { ...dataset, ...style }
21
+ pluginState.layerAdapter?.setStyle(updatedDataset, mapState.mapStyle.id)
22
+ }
@@ -1,8 +1,14 @@
1
- import { handleSetMapStyle } from './handleSetMapStyle.js'
2
- import { addMapLayers, getSourceId, getLayersUsingSource, isDynamicSource, updateSourceData } from './mapLayers.js'
3
1
  import { createDynamicSource } from './fetch/createDynamicSource.js'
2
+ // NOSONAR: applyDatasetDefaults and datasetDefaults are used in processedDatasets.map
3
+ import { applyDatasetDefaults, datasetDefaults } from './defaults.js'
4
+
5
+ const isDynamicSource = (dataset) =>
6
+ typeof dataset.geojson === 'string' &&
7
+ !!dataset.idProperty &&
8
+ typeof dataset.transformRequest === 'function'
4
9
 
5
10
  export const createDatasets = ({
11
+ adapter,
6
12
  pluginConfig,
7
13
  pluginStateRef,
8
14
  mapStyleId,
@@ -10,94 +16,63 @@ export const createDatasets = ({
10
16
  events,
11
17
  eventBus
12
18
  }) => {
13
- const { map } = mapProvider
14
19
  const { datasets } = pluginConfig
15
20
 
16
- // Track dynamic sources for cleanup
17
21
  const dynamicSources = new Map()
18
22
 
19
23
  const getDatasets = () => pluginStateRef.current.datasets || datasets
20
24
  const getHiddenFeatures = () => pluginStateRef.current.hiddenFeatures || {}
21
25
 
22
- // Initialize all datasets once
23
- datasets.forEach(dataset => {
24
- addMapLayers(map, mapStyleId, dataset)
26
+ // Initialise all datasets via the adapter, then set up dynamic sources
27
+ const processedDatasets = datasets.map(d => applyDatasetDefaults(d, datasetDefaults))
28
+ adapter.init(processedDatasets, mapStyleId).then(() => {
29
+ processedDatasets.forEach(dataset => {
30
+ if (!isDynamicSource(dataset)) {
31
+ return
32
+ }
25
33
 
26
- // Initialize dynamic source if applicable
27
- if (isDynamicSource(dataset)) {
28
- const sourceId = getSourceId(dataset)
29
34
  const dynamicSource = createDynamicSource({
30
35
  dataset,
31
- map,
32
- sourceId,
33
- onUpdate: (id, geojson) => updateSourceData(map, id, geojson)
36
+ map: mapProvider.map,
37
+ onUpdate: (datasetId, geojson) => adapter.setData(datasetId, geojson)
34
38
  })
35
39
  dynamicSources.set(dataset.id, dynamicSource)
36
- }
37
- })
40
+ })
38
41
 
39
- // Emit ready event once map has processed the layers
40
- map.once('idle', () => {
41
42
  eventBus.emit('datasets:ready')
42
43
  })
43
44
 
44
- // Handle style changes
45
- const styleHandler = handleSetMapStyle({
46
- map,
47
- events,
48
- eventBus,
49
- getDatasets,
50
- getHiddenFeatures,
51
- getDynamicSources: () => dynamicSources
52
- })
45
+ // Handle basemap style changes — delegate entirely to the adapter
46
+ const onSetStyle = (e) => {
47
+ adapter.onStyleChange(getDatasets(), e.id, getHiddenFeatures(), dynamicSources)
48
+ }
49
+
50
+ eventBus.on(events.MAP_SET_STYLE, onSetStyle)
53
51
 
54
52
  return {
55
- remove() {
56
- eventBus.off(events.MAP_SET_STYLE, styleHandler)
53
+ remove () {
54
+ eventBus.off(events.MAP_SET_STYLE, onSetStyle)
57
55
 
58
56
  // Clean up dynamic sources
59
57
  dynamicSources.forEach(source => source.destroy())
60
58
  dynamicSources.clear()
61
-
62
- const allDatasets = getDatasets()
63
- const removedSourceIds = new Set()
64
-
65
- // Remove layers and sources
66
- allDatasets.forEach(dataset => {
67
- const sourceId = getSourceId(dataset)
68
- const layers = getLayersUsingSource(map, sourceId)
69
-
70
- // Remove all layers using this source
71
- layers.forEach(id => map.removeLayer(id))
72
-
73
- // Remove the source once
74
- if (!removedSourceIds.has(sourceId) && map.getSource(sourceId)) {
75
- map.removeSource(sourceId)
76
- removedSourceIds.add(sourceId)
77
- }
78
- })
59
+ adapter.destroy(getDatasets())
79
60
  },
80
61
 
81
62
  /**
82
63
  * Refresh a dynamic source - clears cache and re-fetches
83
64
  * @param {string} datasetId - Dataset ID to refresh
84
65
  */
85
- refreshDataset(datasetId) {
86
- const dynamicSource = dynamicSources.get(datasetId)
87
- if (dynamicSource) {
88
- dynamicSource.refresh()
89
- }
66
+ refreshDataset (datasetId) {
67
+ dynamicSources.get(datasetId)?.refresh()
90
68
  },
91
69
 
92
70
  /**
93
71
  * Clear a dynamic source's cache
94
72
  * @param {string} datasetId - Dataset ID to clear
95
73
  */
96
- clearDatasetCache(datasetId) {
97
- const dynamicSource = dynamicSources.get(datasetId)
98
- if (dynamicSource) {
99
- dynamicSource.clear()
100
- }
74
+ clearDatasetCache (datasetId) {
75
+ dynamicSources.get(datasetId)?.clear()
101
76
  },
102
77
 
103
78
  /**
@@ -105,9 +80,8 @@ export const createDatasets = ({
105
80
  * @param {string} datasetId - Dataset ID
106
81
  * @returns {number|null} Feature count or null if not a dynamic source
107
82
  */
108
- getFeatureCount(datasetId) {
109
- const dynamicSource = dynamicSources.get(datasetId)
110
- return dynamicSource ? dynamicSource.getFeatureCount() : null
83
+ getFeatureCount (datasetId) {
84
+ return dynamicSources.get(datasetId)?.getFeatureCount() ?? null
111
85
  }
112
86
  }
113
87
  }
@@ -1,15 +1,49 @@
1
1
  const datasetDefaults = {
2
- stroke: '#d4351c',
3
- strokeWidth: 2,
4
- fill: 'transparent',
5
- symbolDescription: 'red outline',
6
2
  minZoom: 6,
7
3
  maxZoom: 24,
8
4
  showInKey: false,
9
- showInLayers: false,
10
- visibility: 'visible'
5
+ toggleVisibility: false,
6
+ visibility: 'visible',
7
+ style: {
8
+ stroke: '#d4351c',
9
+ strokeWidth: 2,
10
+ fill: 'transparent',
11
+ symbolDescription: 'red outline'
12
+ }
11
13
  }
12
14
 
13
- export {
14
- datasetDefaults
15
- }
15
+ // All properties considered style properties — must be provided via dataset.style, not at the top level.
16
+ const STYLE_PROPS = [
17
+ 'stroke', 'strokeWidth', 'strokeDashArray',
18
+ 'fill', 'fillPattern', 'fillPatternSvgContent', 'fillPatternForegroundColor', 'fillPatternBackgroundColor',
19
+ 'opacity', 'symbolDescription', 'keySymbolShape'
20
+ ]
21
+
22
+ // Props whose presence in a style object indicates a custom visual style.
23
+ // When any are set, the default symbolDescription is not appropriate.
24
+ const VISUAL_STYLE_PROPS = ['stroke', 'fill', 'fillPattern', 'fillPatternSvgContent']
25
+
26
+ const hasCustomVisualStyle = (style) =>
27
+ VISUAL_STYLE_PROPS.some(prop => prop in style)
28
+
29
+ /**
30
+ * Merge a dataset config with defaults, flattening the nested `style` object.
31
+ * Style properties must be provided via dataset.style — top-level occurrences are ignored.
32
+ * symbolDescription from defaults.style is dropped when custom visual styles
33
+ * are present and the dataset doesn't explicitly set its own symbolDescription.
34
+ */
35
+ const applyDatasetDefaults = (dataset, defaults) => {
36
+ const style = dataset.style || {}
37
+ const mergedStyle = { ...defaults.style, ...style }
38
+ if (!('symbolDescription' in style) && hasCustomVisualStyle(style)) {
39
+ delete mergedStyle.symbolDescription
40
+ }
41
+ const topLevel = { ...dataset }
42
+ delete topLevel.style
43
+ STYLE_PROPS.forEach(prop => delete topLevel[prop])
44
+ const topLevelDefaults = { ...defaults }
45
+ delete topLevelDefaults.style
46
+ return { ...topLevelDefaults, ...topLevel, ...mergedStyle }
47
+ }
48
+
49
+ export { datasetDefaults, hasCustomVisualStyle, applyDatasetDefaults }
@@ -14,17 +14,17 @@ const EVICTION_THRESHOLD = 1.2 // Trigger eviction at 120% of maxFeatures
14
14
  * @param {Function} options.onUpdate - Callback when source data should be updated
15
15
  * @returns {Object} { destroy, clear, refresh }
16
16
  */
17
- export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
17
+ export const createDynamicSource = ({ dataset, map, onUpdate }) => {
18
18
  const { geojson: baseUrl, idProperty, transformRequest, maxFeatures, minZoom = 0 } = dataset
19
19
 
20
- // Feature cache: id → { feature, bbox, addedAt }
20
+ // Feature cache: id → { feature, bbox, lastSeenAt }
21
21
  const features = new Map()
22
22
 
23
23
  // Track the bbox we've fetched data for
24
24
  let fetchedBbox = null
25
25
 
26
- // Loading state
27
- let isLoading = false
26
+ // Abort controller for the in-flight request
27
+ let currentController = null
28
28
 
29
29
  /**
30
30
  * Convert features Map to FeatureCollection
@@ -63,25 +63,25 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
63
63
  if (bboxIntersects(data.bbox, currentBbox)) {
64
64
  inView.push(id)
65
65
  } else {
66
- outOfView.push({ id, addedAt: data.addedAt })
66
+ outOfView.push({ id, lastSeenAt: data.lastSeenAt })
67
67
  }
68
68
  }
69
69
 
70
- // Sort out-of-view by insertion time (oldest first)
71
- outOfView.sort((a, b) => a.addedAt - b.addedAt)
70
+ // Sort out-of-view by last seen time (least recently seen first)
71
+ outOfView.sort((a, b) => a.lastSeenAt - b.lastSeenAt)
72
72
 
73
- // Evict oldest out-of-view features until under target
73
+ // Evict least-recently-seen out-of-view features until under target
74
74
  const toEvict = features.size - targetSize
75
75
  for (let i = 0; i < toEvict && i < outOfView.length; i++) {
76
76
  features.delete(outOfView[i].id)
77
77
  }
78
78
 
79
- // If still over target (viewport has too many), evict oldest in-view
79
+ // If still over target (viewport has too many), evict least recently seen in-view
80
80
  if (features.size > targetSize) {
81
81
  const remaining = features.size - targetSize
82
82
  const inViewSorted = inView
83
- .map(id => ({ id, addedAt: features.get(id).addedAt }))
84
- .sort((a, b) => a.addedAt - b.addedAt)
83
+ .map(id => ({ id, lastSeenAt: features.get(id).lastSeenAt }))
84
+ .sort((a, b) => a.lastSeenAt - b.lastSeenAt)
85
85
 
86
86
  for (let i = 0; i < remaining && i < inViewSorted.length; i++) {
87
87
  features.delete(inViewSorted[i].id)
@@ -105,19 +105,19 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
105
105
  return
106
106
  }
107
107
 
108
- if (isLoading) {
109
- return
108
+ // Abort any in-flight request — new viewport takes priority
109
+ if (currentController) {
110
+ currentController.abort()
110
111
  }
111
-
112
- isLoading = true
112
+ currentController = new AbortController()
113
113
 
114
114
  try {
115
115
  const context = { bbox: currentBbox, zoom, dataset }
116
- const data = await fetchGeoJSON(baseUrl, context, transformRequest)
116
+ const data = await fetchGeoJSON(baseUrl, context, transformRequest, currentController.signal)
117
117
 
118
118
  const now = Date.now()
119
119
 
120
- // Add/update features with deduplication
120
+ // Add/update features with deduplication, refreshing lastSeenAt on each fetch
121
121
  data.features.forEach(feature => {
122
122
  const id = getFeatureId(feature)
123
123
  if (id == null) {
@@ -128,22 +128,28 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
128
128
  features.set(id, {
129
129
  feature,
130
130
  bbox: getGeometryBbox(feature.geometry),
131
- addedAt: features.has(id) ? features.get(id).addedAt : now
131
+ lastSeenAt: now
132
132
  })
133
133
  })
134
134
 
135
135
  // Expand tracked bbox
136
136
  fetchedBbox = expandBbox(fetchedBbox, currentBbox)
137
137
 
138
- // Evict if over limit
138
+ // Evict if over limit; if features were removed, fetchedBbox no longer
139
+ // covers those regions — reset to current viewport to force re-fetch on return
140
+ const sizeBeforeEviction = features.size
139
141
  evictIfNeeded(currentBbox)
142
+ if (features.size < sizeBeforeEviction) {
143
+ fetchedBbox = currentBbox
144
+ }
140
145
 
141
146
  // Update map source
142
- onUpdate(sourceId, toFeatureCollection())
147
+ onUpdate(dataset.id, toFeatureCollection())
143
148
  } catch (error) {
149
+ if (error.name === 'AbortError') {
150
+ return
151
+ }
144
152
  console.error(`Failed to fetch dynamic GeoJSON for ${dataset.id}:`, error)
145
- } finally {
146
- isLoading = false
147
153
  }
148
154
  }
149
155
 
@@ -162,26 +168,29 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
162
168
 
163
169
  return {
164
170
  /**
165
- * Clean up event listeners
171
+ * Clean up event listeners and cancel any in-flight request
166
172
  */
167
- destroy() {
173
+ destroy () {
168
174
  map.off('moveend', handleMoveEnd)
169
175
  debouncedFetch.cancel()
176
+ if (currentController) {
177
+ currentController.abort()
178
+ }
170
179
  },
171
180
 
172
181
  /**
173
182
  * Clear all cached features and reset fetch tracking
174
183
  */
175
- clear() {
184
+ clear () {
176
185
  features.clear()
177
186
  fetchedBbox = null
178
- onUpdate(sourceId, { type: 'FeatureCollection', features: [] })
187
+ onUpdate(dataset.id, { type: 'FeatureCollection', features: [] })
179
188
  },
180
189
 
181
190
  /**
182
191
  * Force refresh - clear cache and fetch current viewport
183
192
  */
184
- refresh() {
193
+ refresh () {
185
194
  features.clear()
186
195
  fetchedBbox = null
187
196
  fetchData()
@@ -190,16 +199,16 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
190
199
  /**
191
200
  * Get current feature count
192
201
  */
193
- getFeatureCount() {
202
+ getFeatureCount () {
194
203
  return features.size
195
204
  },
196
205
 
197
206
  /**
198
207
  * Re-push cached features to the source (e.g., after style change)
199
208
  */
200
- reapply() {
209
+ reapply () {
201
210
  if (features.size > 0) {
202
- onUpdate(sourceId, toFeatureCollection())
211
+ onUpdate(dataset.id, toFeatureCollection())
203
212
  }
204
213
  }
205
214
  }
@@ -5,14 +5,14 @@
5
5
  * @param {Function} transformRequest - Function to transform the request (builds URL with bbox, adds headers)
6
6
  * @returns {Promise<Object>} GeoJSON FeatureCollection
7
7
  */
8
- export const fetchGeoJSON = async (baseUrl, context, transformRequest) => {
8
+ export const fetchGeoJSON = async (baseUrl, context, transformRequest, signal) => {
9
9
  const result = transformRequest(baseUrl, context)
10
10
 
11
11
  // Handle both string and object return values
12
12
  const config = typeof result === 'string' ? { url: result } : result
13
13
  const { url, headers = {} } = config
14
14
 
15
- const response = await fetch(url, { headers })
15
+ const response = await fetch(url, { headers, signal })
16
16
 
17
17
  if (!response.ok) {
18
18
  throw new Error(`Failed to fetch GeoJSON: ${response.status} ${response.statusText}`)
@@ -3,12 +3,15 @@ import { initialState, actions } from './reducer.js'
3
3
  import { DatasetsInit } from './DatasetsInit.jsx'
4
4
  import { Layers } from './panels/Layers.jsx'
5
5
  import { Key } from './panels/Key.jsx'
6
- import { showDataset } from './api/showDataset.js'
7
- import { hideDataset } from './api/hideDataset.js'
8
6
  import { addDataset } from './api/addDataset.js'
9
7
  import { removeDataset } from './api/removeDataset.js'
10
- import { showFeatures } from './api/showFeatures.js'
11
- import { hideFeatures } from './api/hideFeatures.js'
8
+ import { setDatasetVisibility } from './api/setDatasetVisibility.js'
9
+ import { setFeatureVisibility } from './api/setFeatureVisibility.js'
10
+ import { setStyle } from './api/setStyle.js'
11
+ import { getStyle } from './api/getStyle.js'
12
+ import { setOpacity } from './api/setOpacity.js'
13
+ import { getOpacity } from './api/getOpacity.js'
14
+ import { setData } from './api/setData.js'
12
15
 
13
16
  export const manifest = {
14
17
  InitComponent: DatasetsInit,
@@ -22,7 +25,7 @@ export const manifest = {
22
25
  id: 'datasetsLayers',
23
26
  label: 'Layers',
24
27
  mobile: {
25
- slot: 'bottom',
28
+ slot: 'drawer',
26
29
  modal: true,
27
30
  dismissible: true
28
31
  },
@@ -30,30 +33,30 @@ export const manifest = {
30
33
  slot: 'left-top',
31
34
  dismissible: true,
32
35
  exclusive: true,
33
- width: '300px'
36
+ width: '260px'
34
37
  },
35
38
  desktop: {
36
39
  slot: 'left-top',
37
40
  modal: false,
38
41
  dismissible: true,
39
42
  exclusive: true,
40
- width: '320px'
43
+ width: '280px'
41
44
  },
42
45
  render: Layers
43
- },{
46
+ }, {
44
47
  id: 'datasetsKey',
45
48
  label: 'Key',
46
49
  mobile: {
47
- slot: 'bottom',
50
+ slot: 'drawer',
48
51
  modal: true
49
52
  },
50
53
  tablet: {
51
54
  slot: 'left-top',
52
- width: '300px'
55
+ width: '260px'
53
56
  },
54
57
  desktop: {
55
58
  slot: 'left-top',
56
- width: '320px'
59
+ width: '280px'
57
60
  },
58
61
  render: Key
59
62
  }],
@@ -63,7 +66,9 @@ export const manifest = {
63
66
  label: 'Layers',
64
67
  panelId: 'datasetsLayers',
65
68
  iconId: 'layers',
66
- excludeWhen: ({ pluginConfig }) => !pluginConfig.datasets.find(l => l.showInLayers),
69
+ excludeWhen: ({ pluginConfig }) => !pluginConfig.datasets.some(l =>
70
+ l.toggleVisibility || l.sublayers?.some(r => r.toggleVisibility)
71
+ ),
67
72
  mobile: {
68
73
  slot: 'top-left',
69
74
  showLabel: true
@@ -76,12 +81,12 @@ export const manifest = {
76
81
  slot: 'top-left',
77
82
  showLabel: true
78
83
  }
79
- },{
84
+ }, {
80
85
  id: 'datasetsKey',
81
86
  label: 'Key',
82
87
  panelId: 'datasetsKey',
83
88
  iconId: 'key',
84
- excludeWhen: ({ pluginConfig }) => !pluginConfig.datasets.find(l => l.showInKey),
89
+ excludeWhen: ({ pluginConfig }) => !pluginConfig.datasets.some(l => l.showInKey),
85
90
  mobile: {
86
91
  slot: 'top-left',
87
92
  showLabel: false
@@ -99,17 +104,20 @@ export const manifest = {
99
104
  icons: [{
100
105
  id: 'layers',
101
106
  svgContent: '<path d="M13 13.74a2 2 0 0 1-2 0L2.5 8.87a1 1 0 0 1 0-1.74L11 2.26a2 2 0 0 1 2 0l8.5 4.87a1 1 0 0 1 0 1.74z"></path><path d="m20 14.285 1.5.845a1 1 0 0 1 0 1.74L13 21.74a2 2 0 0 1-2 0l-8.5-4.87a1 1 0 0 1 0-1.74l1.5-.845"></path>'
102
- },{
107
+ }, {
103
108
  id: 'key',
104
109
  svgContent: '<path d="M3 5h.01"/><path d="M3 12h.01"/><path d="M3 19h.01"/><path d="M8 5h13"/><path d="M8 12h13"/><path d="M8 19h13"/>'
105
110
  }],
106
111
 
107
112
  api: {
108
- showDataset,
109
- hideDataset,
110
113
  addDataset,
111
114
  removeDataset,
112
- showFeatures,
113
- hideFeatures
115
+ setDatasetVisibility,
116
+ setFeatureVisibility,
117
+ setStyle,
118
+ getStyle,
119
+ setOpacity,
120
+ getOpacity,
121
+ setData
114
122
  }
115
123
  }