@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,113 @@
1
+ import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js'
2
+ import { hasPattern, getPatternImageId } from '../../styles/patterns.js'
3
+ import { mergeSublayer } from '../../utils/mergeSublayer.js'
4
+ import { getSourceId, getLayerIds, getSublayerLayerIds, isDynamicSource, MAX_TILE_ZOOM } from './layerIds.js'
5
+
6
+ // ─── Source ───────────────────────────────────────────────────────────────────
7
+
8
+ export const addSource = (map, dataset, sourceId) => {
9
+ if (map.getSource(sourceId)) {
10
+ return
11
+ }
12
+ if (dataset.tiles) {
13
+ map.addSource(sourceId, {
14
+ type: 'vector',
15
+ tiles: dataset.tiles,
16
+ minzoom: dataset.minZoom || 0,
17
+ maxzoom: dataset.maxZoom || MAX_TILE_ZOOM
18
+ })
19
+ return
20
+ }
21
+ if (dataset.geojson) {
22
+ const initialData = isDynamicSource(dataset)
23
+ ? { type: 'FeatureCollection', features: [] }
24
+ : dataset.geojson
25
+ map.addSource(sourceId, { type: 'geojson', data: initialData, generateId: true })
26
+ }
27
+ }
28
+
29
+ // ─── Fill layer ───────────────────────────────────────────────────────────────
30
+
31
+ export const addFillLayer = (map, config, layerId, sourceId, sourceLayer, visibility, mapStyleId) => {
32
+ if (!layerId || map.getLayer(layerId)) {
33
+ return
34
+ }
35
+ if (!config.fill && !hasPattern(config)) {
36
+ return
37
+ }
38
+ const patternImageId = hasPattern(config) ? getPatternImageId(config, mapStyleId) : null
39
+ const paint = patternImageId
40
+ ? { 'fill-pattern': patternImageId, 'fill-opacity': config.opacity || 1 }
41
+ : { 'fill-color': getValueForStyle(config.fill, mapStyleId), 'fill-opacity': config.opacity || 1 }
42
+ map.addLayer({
43
+ id: layerId,
44
+ type: 'fill',
45
+ source: sourceId,
46
+ 'source-layer': sourceLayer,
47
+ layout: { visibility },
48
+ paint,
49
+ ...(config.filter ? { filter: config.filter } : {})
50
+ })
51
+ }
52
+
53
+ // ─── Stroke layer ─────────────────────────────────────────────────────────────
54
+
55
+ export const addStrokeLayer = (map, config, layerId, sourceId, sourceLayer, visibility, mapStyleId) => {
56
+ if (!layerId || !config.stroke || map.getLayer(layerId)) {
57
+ return
58
+ }
59
+ map.addLayer({
60
+ id: layerId,
61
+ type: 'line',
62
+ source: sourceId,
63
+ 'source-layer': sourceLayer,
64
+ layout: { visibility },
65
+ paint: {
66
+ 'line-color': getValueForStyle(config.stroke, mapStyleId),
67
+ 'line-width': config.strokeWidth || 1,
68
+ 'line-opacity': config.opacity || 1,
69
+ ...(config.strokeDashArray ? { 'line-dasharray': config.strokeDashArray } : {})
70
+ },
71
+ ...(config.filter ? { filter: config.filter } : {})
72
+ })
73
+ }
74
+
75
+ // ─── Dataset layers ───────────────────────────────────────────────────────────
76
+
77
+ export const addSublayerLayers = (map, dataset, sublayer, sourceId, sourceLayer, mapStyleId) => {
78
+ const merged = mergeSublayer(dataset, sublayer)
79
+ const { fillLayerId, strokeLayerId } = getSublayerLayerIds(dataset.id, sublayer.id)
80
+ const parentHidden = dataset.visibility === 'hidden'
81
+ const sublayerHidden = dataset.sublayerVisibility?.[sublayer.id] === 'hidden'
82
+ const visibility = (parentHidden || sublayerHidden) ? 'none' : 'visible'
83
+ addFillLayer(map, merged, fillLayerId, sourceId, sourceLayer, visibility, mapStyleId)
84
+ addStrokeLayer(map, merged, strokeLayerId, sourceId, sourceLayer, visibility, mapStyleId)
85
+ }
86
+
87
+ /**
88
+ * Add all layers (and source if needed) for a dataset.
89
+ * Returns the sourceId so the caller can track the datasetId → sourceId mapping.
90
+ * @param {Object} map - MapLibre map instance
91
+ * @param {Object} dataset
92
+ * @param {string} mapStyleId
93
+ * @returns {string} sourceId
94
+ */
95
+ export const addDatasetLayers = (map, dataset, mapStyleId) => {
96
+ const sourceId = getSourceId(dataset)
97
+ addSource(map, dataset, sourceId)
98
+
99
+ const sourceLayer = dataset.tiles?.length ? dataset.sourceLayer : undefined
100
+
101
+ if (dataset.sublayers?.length) {
102
+ dataset.sublayers.forEach(sublayer => {
103
+ addSublayerLayers(map, dataset, sublayer, sourceId, sourceLayer, mapStyleId)
104
+ })
105
+ return sourceId
106
+ }
107
+
108
+ const { fillLayerId, strokeLayerId } = getLayerIds(dataset)
109
+ const visibility = dataset.visibility === 'hidden' ? 'none' : 'visible'
110
+ addFillLayer(map, dataset, fillLayerId, sourceId, sourceLayer, visibility, mapStyleId)
111
+ addStrokeLayer(map, dataset, strokeLayerId, sourceId, sourceLayer, visibility, mapStyleId)
112
+ return sourceId
113
+ }
@@ -0,0 +1,69 @@
1
+ import { hasPattern } from '../../styles/patterns.js'
2
+
3
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
4
+
5
+ export const isDynamicSource = (dataset) =>
6
+ typeof dataset.geojson === 'string' &&
7
+ !!dataset.idProperty &&
8
+ typeof dataset.transformRequest === 'function'
9
+
10
+ const HASH_BASE = 36
11
+ const MAX_TILE_ZOOM = 22
12
+
13
+ export { MAX_TILE_ZOOM }
14
+
15
+ export const hashString = (str) => {
16
+ let hash = 0
17
+ for (const ch of str) {
18
+ hash = Math.trunc(((hash << 5) - hash) + ch.codePointAt(0))
19
+ }
20
+ return Math.abs(hash).toString(HASH_BASE)
21
+ }
22
+
23
+ // ─── Source ID ────────────────────────────────────────────────────────────────
24
+
25
+ export const getSourceId = (dataset) => {
26
+ if (dataset.tiles) {
27
+ const tilesKey = Array.isArray(dataset.tiles) ? dataset.tiles.join(',') : dataset.tiles
28
+ return `tiles-${hashString(tilesKey)}`
29
+ }
30
+ if (dataset.geojson) {
31
+ if (isDynamicSource(dataset)) {
32
+ return `geojson-dynamic-${dataset.id}`
33
+ }
34
+ if (typeof dataset.geojson === 'string') {
35
+ return `geojson-${hashString(dataset.geojson)}`
36
+ }
37
+ return `geojson-${dataset.id}`
38
+ }
39
+ return `source-${dataset.id}`
40
+ }
41
+
42
+ // ─── Layer IDs ────────────────────────────────────────────────────────────────
43
+
44
+ export const getLayerIds = (dataset) => {
45
+ const hasFill = !!dataset.fill || hasPattern(dataset)
46
+ const hasStroke = !!dataset.stroke
47
+ const fillLayerId = hasFill ? dataset.id : null
48
+ let strokeLayerId = null
49
+ if (hasStroke) {
50
+ strokeLayerId = hasFill ? `${dataset.id}-stroke` : dataset.id
51
+ }
52
+ return { fillLayerId, strokeLayerId }
53
+ }
54
+
55
+ export const getSublayerLayerIds = (datasetId, sublayerId) => ({
56
+ fillLayerId: `${datasetId}-${sublayerId}`,
57
+ strokeLayerId: `${datasetId}-${sublayerId}-stroke`
58
+ })
59
+
60
+ export const getAllLayerIds = (dataset) => {
61
+ if (dataset.sublayers?.length) {
62
+ return dataset.sublayers.flatMap(sublayer => {
63
+ const { fillLayerId, strokeLayerId } = getSublayerLayerIds(dataset.id, sublayer.id)
64
+ return [strokeLayerId, fillLayerId]
65
+ })
66
+ }
67
+ const { fillLayerId, strokeLayerId } = getLayerIds(dataset)
68
+ return [strokeLayerId, fillLayerId].filter(Boolean)
69
+ }
@@ -0,0 +1,338 @@
1
+ import { applyExclusionFilter } from '../../utils/filters.js'
2
+ import { getSourceId, getLayerIds, getSublayerLayerIds, getAllLayerIds } from './layerIds.js'
3
+ import { addDatasetLayers, addSublayerLayers } from './layerBuilders.js'
4
+ import { registerPatterns } from './patternRegistry.js'
5
+
6
+ /**
7
+ * MapLibre GL JS implementation of the LayerAdapter interface for the datasets plugin.
8
+ *
9
+ * Owns all map-framework-specific concerns:
10
+ * - Source and layer creation (delegated to layerBuilders)
11
+ * - Pattern image registration (delegated to patternRegistry)
12
+ * - Visibility toggling, feature filtering, style changes
13
+ * - Style-change recovery (re-adding layers after basemap swap)
14
+ */
15
+ export default class MaplibreLayerAdapter {
16
+ constructor (map) {
17
+ this._map = map
18
+ // datasetId → sourceId, used by setData to update the correct source
19
+ this._datasetSourceMap = new Map()
20
+ }
21
+
22
+ // ─── Lifecycle ──────────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Initialise all datasets: register patterns, add layers, then wait for idle.
26
+ * @param {Object[]} datasets
27
+ * @param {string} mapStyleId
28
+ * @returns {Promise<void>} Resolves once the map has processed all layers.
29
+ */
30
+ async init (datasets, mapStyleId) {
31
+ await registerPatterns(this._map, datasets, mapStyleId)
32
+ datasets.forEach(dataset => this._addLayers(dataset, mapStyleId))
33
+ await new Promise(resolve => this._map.once('idle', resolve))
34
+ }
35
+
36
+ /**
37
+ * Remove all layers and sources for the given datasets.
38
+ * @param {Object[]} datasets
39
+ */
40
+ destroy (datasets) {
41
+ const removedSourceIds = new Set()
42
+ datasets.forEach(dataset => {
43
+ const sourceId = getSourceId(dataset)
44
+ this._getLayersUsingSource(sourceId).forEach(layerId => {
45
+ if (this._map.getLayer(layerId)) {
46
+ this._map.removeLayer(layerId)
47
+ }
48
+ })
49
+ if (!removedSourceIds.has(sourceId) && this._map.getSource(sourceId)) {
50
+ this._map.removeSource(sourceId)
51
+ removedSourceIds.add(sourceId)
52
+ }
53
+ })
54
+ this._datasetSourceMap.clear()
55
+ }
56
+
57
+ /**
58
+ * Re-register patterns and re-add all layers after a basemap style change,
59
+ * then reapply cached dynamic source data and hidden-feature filters.
60
+ * @param {Object[]} datasets
61
+ * @param {string} newStyleId
62
+ * @param {Object} hiddenFeatures - pluginState.hiddenFeatures
63
+ * @param {Map} dynamicSources - datasetId → dynamic source instance
64
+ * @returns {Promise<void>}
65
+ */
66
+ async onStyleChange (datasets, newStyleId, hiddenFeatures, dynamicSources) {
67
+ // MapLibre wipes all sources/layers on style change — must wait for idle first
68
+ await new Promise(resolve => this._map.once('idle', resolve))
69
+
70
+ await registerPatterns(this._map, datasets, newStyleId)
71
+ datasets.forEach(dataset => this._addLayers(dataset, newStyleId))
72
+
73
+ // Re-push cached data for dynamic sources
74
+ dynamicSources.forEach(source => source.reapply())
75
+
76
+ // Reapply hidden feature filters
77
+ Object.entries(hiddenFeatures).forEach(([datasetId, { idProperty, ids }]) => {
78
+ const dataset = datasets.find(d => d.id === datasetId)
79
+ if (!dataset) {
80
+ return
81
+ }
82
+ this._applyFeatureFilter(dataset, idProperty, ids)
83
+ })
84
+ }
85
+
86
+ // ─── Dataset operations ─────────────────────────────────────────────────────
87
+
88
+ /**
89
+ * Add a single dataset's source and layers to the map.
90
+ * @param {Object} dataset
91
+ * @param {string} mapStyleId
92
+ */
93
+ addDataset (dataset, mapStyleId) {
94
+ this._addLayers(dataset, mapStyleId)
95
+ }
96
+
97
+ /**
98
+ * Remove a dataset's layers and source from the map.
99
+ * Shared sources (same tiles URL or geojson URL used by multiple datasets) are
100
+ * only removed when no other remaining dataset references them.
101
+ * @param {Object} dataset
102
+ * @param {Object[]} allDatasets - Full current dataset list, for shared-source check.
103
+ */
104
+ removeDataset (dataset, allDatasets) {
105
+ const sourceId = getSourceId(dataset)
106
+
107
+ getAllLayerIds(dataset).forEach(layerId => {
108
+ if (this._map.getLayer(layerId)) {
109
+ this._map.removeLayer(layerId)
110
+ }
111
+ })
112
+
113
+ const sourceIsShared = allDatasets.some(d => d.id !== dataset.id && getSourceId(d) === sourceId)
114
+
115
+ if (!sourceIsShared && this._map.getSource(sourceId)) {
116
+ this._map.removeSource(sourceId)
117
+ }
118
+
119
+ this._datasetSourceMap.delete(dataset.id)
120
+ }
121
+
122
+ /**
123
+ * Make a dataset's layers visible.
124
+ * @param {string} datasetId
125
+ */
126
+ showDataset (datasetId) {
127
+ this._setDatasetVisibility(datasetId, 'visible')
128
+ }
129
+
130
+ /**
131
+ * Hide a dataset's layers.
132
+ * @param {string} datasetId
133
+ */
134
+ hideDataset (datasetId) {
135
+ this._setDatasetVisibility(datasetId, 'none')
136
+ }
137
+
138
+ /**
139
+ * Make a single sublayer's layers visible.
140
+ * @param {string} datasetId
141
+ * @param {string} sublayerId
142
+ */
143
+ showSublayer (datasetId, sublayerId) {
144
+ const { fillLayerId, strokeLayerId } = getSublayerLayerIds(datasetId, sublayerId)
145
+ if (this._map.getLayer(fillLayerId)) {
146
+ this._map.setLayoutProperty(fillLayerId, 'visibility', 'visible')
147
+ }
148
+ if (this._map.getLayer(strokeLayerId)) {
149
+ this._map.setLayoutProperty(strokeLayerId, 'visibility', 'visible')
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Hide a single sublayer's layers.
155
+ * @param {string} datasetId
156
+ * @param {string} sublayerId
157
+ */
158
+ hideSublayer (datasetId, sublayerId) {
159
+ const { fillLayerId, strokeLayerId } = getSublayerLayerIds(datasetId, sublayerId)
160
+ if (this._map.getLayer(fillLayerId)) {
161
+ this._map.setLayoutProperty(fillLayerId, 'visibility', 'none')
162
+ }
163
+ if (this._map.getLayer(strokeLayerId)) {
164
+ this._map.setLayoutProperty(strokeLayerId, 'visibility', 'none')
165
+ }
166
+ }
167
+
168
+ // ─── Feature operations ─────────────────────────────────────────────────────
169
+
170
+ /**
171
+ * Show previously hidden features by updating the layer exclusion filter.
172
+ * @param {Object} dataset
173
+ * @param {string} idProperty
174
+ * @param {Array} remainingHiddenIds - IDs that should remain hidden after this call.
175
+ */
176
+ showFeatures (dataset, idProperty, remainingHiddenIds) {
177
+ this._applyFeatureFilter(dataset, idProperty, remainingHiddenIds)
178
+ }
179
+
180
+ /**
181
+ * Hide features by updating the layer exclusion filter.
182
+ * @param {Object} dataset
183
+ * @param {string} idProperty
184
+ * @param {Array} allHiddenIds - Full set of IDs to hide (existing + new).
185
+ */
186
+ hideFeatures (dataset, idProperty, allHiddenIds) {
187
+ this._applyFeatureFilter(dataset, idProperty, allHiddenIds)
188
+ }
189
+
190
+ /**
191
+ * Update a dataset's style and re-render all its layers.
192
+ * @param {Object} dataset - Updated dataset (style changes already merged in)
193
+ * @param {string} mapStyleId
194
+ * @returns {Promise<void>}
195
+ */
196
+ async setStyle (dataset, mapStyleId) {
197
+ getAllLayerIds(dataset).forEach(layerId => {
198
+ if (this._map.getLayer(layerId)) {
199
+ this._map.removeLayer(layerId)
200
+ }
201
+ })
202
+ await registerPatterns(this._map, [dataset], mapStyleId)
203
+ this._addLayers(dataset, mapStyleId)
204
+ }
205
+
206
+ /**
207
+ * Update a single sublayer's style and re-render its layers.
208
+ * @param {Object} dataset - Updated dataset (sublayer style changes already merged in)
209
+ * @param {string} sublayerId
210
+ * @param {string} mapStyleId
211
+ * @returns {Promise<void>}
212
+ */
213
+ async setSublayerStyle (dataset, sublayerId, mapStyleId) {
214
+ const { fillLayerId, strokeLayerId } = getSublayerLayerIds(dataset.id, sublayerId)
215
+ if (this._map.getLayer(fillLayerId)) {
216
+ this._map.removeLayer(fillLayerId)
217
+ }
218
+ if (this._map.getLayer(strokeLayerId)) {
219
+ this._map.removeLayer(strokeLayerId)
220
+ }
221
+ const sublayer = dataset.sublayers?.find(s => s.id === sublayerId)
222
+ if (!sublayer) {
223
+ return
224
+ }
225
+ await registerPatterns(this._map, [dataset], mapStyleId)
226
+ const sourceId = this._datasetSourceMap.get(dataset.id)
227
+ const sourceLayer = dataset.tiles?.length ? dataset.sourceLayer : undefined
228
+ addSublayerLayers(this._map, dataset, sublayer, sourceId, sourceLayer, mapStyleId)
229
+ }
230
+
231
+ /**
232
+ * Set opacity for all layers belonging to a dataset.
233
+ * Uses setPaintProperty directly — safe to call on every slider tick.
234
+ * @param {string} datasetId
235
+ * @param {number} opacity
236
+ */
237
+ setOpacity (datasetId, opacity) {
238
+ const style = this._map.getStyle()
239
+ if (!style?.layers) {
240
+ return
241
+ }
242
+ style.layers
243
+ .filter(layer => layer.id === datasetId || layer.id.startsWith(`${datasetId}-`))
244
+ .forEach(layer => this._setPaintOpacity(layer.id, opacity))
245
+ }
246
+
247
+ /**
248
+ * Set opacity for a single sublayer's fill and stroke layers.
249
+ * Uses setPaintProperty directly — safe to call on every slider tick.
250
+ * @param {string} datasetId
251
+ * @param {string} sublayerId
252
+ * @param {number} opacity
253
+ */
254
+ setSublayerOpacity (datasetId, sublayerId, opacity) {
255
+ const { fillLayerId, strokeLayerId } = getSublayerLayerIds(datasetId, sublayerId)
256
+ this._setPaintOpacity(fillLayerId, opacity)
257
+ this._setPaintOpacity(strokeLayerId, opacity)
258
+ }
259
+
260
+ /**
261
+ * Update the GeoJSON data for a dataset's source.
262
+ * @param {string} datasetId
263
+ * @param {Object} geojson - GeoJSON FeatureCollection
264
+ */
265
+ setData (datasetId, geojson) {
266
+ const sourceId = this._datasetSourceMap.get(datasetId)
267
+ if (!sourceId) {
268
+ return
269
+ }
270
+ const source = this._map.getSource(sourceId)
271
+ if (source && typeof source.setData === 'function') {
272
+ source.setData(geojson)
273
+ }
274
+ }
275
+
276
+ // ─── Private ─────────────────────────────────────────────────────────────────
277
+
278
+ _addLayers (dataset, mapStyleId) {
279
+ const sourceId = addDatasetLayers(this._map, dataset, mapStyleId)
280
+ this._datasetSourceMap.set(dataset.id, sourceId)
281
+ }
282
+
283
+ _setDatasetVisibility (datasetId, visibility) {
284
+ const style = this._map.getStyle()
285
+ if (!style?.layers) {
286
+ return
287
+ }
288
+ // Covers base fill layer (datasetId) and all suffixed layers
289
+ // (-stroke, -${sublayerId}, -${sublayerId}-stroke) without needing the dataset object.
290
+ style.layers
291
+ .filter(layer =>
292
+ layer.id === datasetId ||
293
+ layer.id.startsWith(`${datasetId}-`)
294
+ )
295
+ .forEach(layer => this._map.setLayoutProperty(layer.id, 'visibility', visibility))
296
+ }
297
+
298
+ _applyFeatureFilter (dataset, idProperty, excludeIds) {
299
+ if (dataset.sublayers?.length) {
300
+ dataset.sublayers.forEach(sublayer => {
301
+ const { fillLayerId: subFillId, strokeLayerId: subStrokeId } = getSublayerLayerIds(dataset.id, sublayer.id)
302
+ const sublayerFilter = dataset.filter && sublayer.filter
303
+ ? ['all', dataset.filter, sublayer.filter]
304
+ : (sublayer.filter || dataset.filter || null)
305
+ applyExclusionFilter(this._map, subFillId, sublayerFilter, idProperty, excludeIds)
306
+ applyExclusionFilter(this._map, subStrokeId, sublayerFilter, idProperty, excludeIds)
307
+ })
308
+ return
309
+ }
310
+ const { fillLayerId, strokeLayerId } = getLayerIds(dataset)
311
+ const originalFilter = dataset.filter || null
312
+ if (fillLayerId) {
313
+ applyExclusionFilter(this._map, fillLayerId, originalFilter, idProperty, excludeIds)
314
+ }
315
+ if (strokeLayerId) {
316
+ applyExclusionFilter(this._map, strokeLayerId, originalFilter, idProperty, excludeIds)
317
+ }
318
+ }
319
+
320
+ _setPaintOpacity (layerId, opacity) {
321
+ const layer = this._map.getLayer(layerId)
322
+ if (!layer) {
323
+ return
324
+ }
325
+ const prop = layer.type === 'line' ? 'line-opacity' : 'fill-opacity'
326
+ this._map.setPaintProperty(layerId, prop, opacity)
327
+ }
328
+
329
+ _getLayersUsingSource (sourceId) {
330
+ const style = this._map.getStyle()
331
+ if (!style?.layers) {
332
+ return []
333
+ }
334
+ return style.layers
335
+ .filter(layer => layer.source === sourceId)
336
+ .map(layer => layer.id)
337
+ }
338
+ }
@@ -0,0 +1,48 @@
1
+ import { hasPattern, getPatternImageId, rasterisePattern } from '../../styles/patterns.js'
2
+ import { mergeSublayer } from '../../utils/mergeSublayer.js'
3
+
4
+ /**
5
+ * Collect all style configs that require a pattern image: top-level datasets
6
+ * and any sublayers whose merged style has a pattern.
7
+ * @param {Object[]} datasets
8
+ * @returns {Object[]}
9
+ */
10
+ const getPatternConfigs = (datasets) =>
11
+ datasets.flatMap(dataset => {
12
+ const configs = hasPattern(dataset) ? [dataset] : []
13
+ if (dataset.sublayers?.length) {
14
+ dataset.sublayers.forEach(sublayer => {
15
+ const merged = mergeSublayer(dataset, sublayer)
16
+ if (hasPattern(merged)) {
17
+ configs.push(merged)
18
+ }
19
+ })
20
+ }
21
+ return configs
22
+ })
23
+
24
+ /**
25
+ * Register all required pattern images with the map.
26
+ * Skips images that are already registered (safe to call on style change).
27
+ * @param {Object} map - MapLibre map instance
28
+ * @param {Object[]} datasets
29
+ * @param {string} mapStyleId
30
+ * @returns {Promise<void>}
31
+ */
32
+ export const registerPatterns = async (map, datasets, mapStyleId) => {
33
+ const patternConfigs = getPatternConfigs(datasets)
34
+ if (!patternConfigs.length) {
35
+ return
36
+ }
37
+
38
+ await Promise.all(patternConfigs.map(async (config) => {
39
+ const imageId = getPatternImageId(config, mapStyleId)
40
+ if (!imageId || map.hasImage(imageId)) {
41
+ return
42
+ }
43
+ const result = await rasterisePattern(config, mapStyleId)
44
+ if (result) {
45
+ map.addImage(result.imageId, result.imageData, { pixelRatio: 2 })
46
+ }
47
+ }))
48
+ }
@@ -1,12 +1,6 @@
1
1
  import { datasetDefaults } from '../defaults.js'
2
- import { addMapLayers } from '../mapLayers.js'
3
2
 
4
- export const addDataset = ({ mapProvider, mapState, pluginState }, dataset) => {
5
- const map = mapProvider.map
6
-
7
- // Add source and layers to the map
8
- addMapLayers(map, mapState.mapStyle.id, { ...datasetDefaults, ...dataset })
9
-
10
- // Update state
3
+ export const addDataset = ({ pluginState, mapState }, dataset) => {
4
+ pluginState.layerAdapter?.addDataset({ ...datasetDefaults, ...dataset }, mapState.mapStyle.id)
11
5
  pluginState.dispatch({ type: 'ADD_DATASET', payload: { dataset, datasetDefaults } })
12
- }
6
+ }
@@ -0,0 +1,17 @@
1
+ export const getOpacity = ({ pluginState }, options) => {
2
+ const { datasetId, sublayerId } = options || {}
3
+
4
+ if (sublayerId) {
5
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
6
+ const sublayer = dataset?.sublayers?.find(s => s.id === sublayerId)
7
+ return sublayer?.style?.opacity ?? null
8
+ }
9
+
10
+ if (datasetId) {
11
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
12
+ return dataset?.opacity ?? null
13
+ }
14
+
15
+ // Global — return first dataset's opacity
16
+ return pluginState.datasets?.[0]?.opacity ?? null
17
+ }
@@ -0,0 +1,13 @@
1
+ export const getStyle = ({ pluginState }, { datasetId, sublayerId } = {}) => {
2
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
3
+ if (!dataset) {
4
+ return null
5
+ }
6
+
7
+ if (sublayerId) {
8
+ const sublayer = dataset.sublayers?.find(s => s.id === sublayerId)
9
+ return sublayer?.style ?? null
10
+ }
11
+
12
+ return dataset.style ?? null
13
+ }
@@ -1,51 +1,9 @@
1
- import { getSourceId } from '../mapLayers.js'
2
-
3
- const getLayerIds = (dataset) => {
4
- const hasFill = !!dataset.fill
5
- const hasStroke = !!dataset.stroke
6
-
7
- const fillLayerId = hasFill ? dataset.id : null
8
- let strokeLayerId = null
9
- if (hasStroke) {
10
- if (hasFill) {
11
- strokeLayerId = `${dataset.id}-stroke`
12
- } else {
13
- strokeLayerId = dataset.id
14
- }
15
- }
16
-
17
- return { fillLayerId, strokeLayerId }
18
- }
19
-
20
- export const removeDataset = ({ mapProvider, pluginState }, datasetId) => {
21
- const map = mapProvider.map
22
-
23
- // Find the dataset
1
+ export const removeDataset = ({ pluginState }, datasetId) => {
24
2
  const dataset = pluginState.datasets?.find(d => d.id === datasetId)
25
3
  if (!dataset) {
26
4
  return
27
5
  }
28
6
 
29
- // Compute layer IDs
30
- const { fillLayerId, strokeLayerId } = getLayerIds(dataset)
31
- const sourceId = getSourceId(dataset)
32
-
33
- // Remove layers first
34
- const layerIdsToRemove = [strokeLayerId, fillLayerId]
35
- layerIdsToRemove.forEach(layerId => {
36
- if (layerId && map.getLayer(layerId)) {
37
- map.removeLayer(layerId)
38
- }
39
- })
40
-
41
- // Remove source if no other datasets use it
42
- const otherDatasetsUseSource = pluginState.datasets?.some(
43
- d => d.id !== datasetId && getSourceId(d) === sourceId
44
- )
45
- if (!otherDatasetsUseSource && map.getSource(sourceId)) {
46
- map.removeSource(sourceId)
47
- }
48
-
49
- // Update plugin state
7
+ pluginState.layerAdapter?.removeDataset(dataset, pluginState.datasets)
50
8
  pluginState.dispatch({ type: 'REMOVE_DATASET', payload: { id: datasetId } })
51
- }
9
+ }
@@ -0,0 +1,8 @@
1
+ export const setData = ({ pluginState, services }, geojson, { datasetId }) => {
2
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
3
+ if (dataset?.tiles) {
4
+ services.logger.warn(`setData called on vector tile dataset "${datasetId}" — has no effect`)
5
+ return
6
+ }
7
+ pluginState.layerAdapter?.setData(datasetId, geojson)
8
+ }