@defra/interactive-map 0.0.16-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 (130) hide show
  1. package/assets/images/slot-map.svg +264 -0
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/slots.md +16 -15
  8. package/docs/api.md +3 -3
  9. package/docs/getting-started.md +4 -1
  10. package/docs/plugins/datasets.md +561 -0
  11. package/docs/plugins.md +1 -1
  12. package/package.json +2 -2
  13. package/plugins/beta/datasets/dist/css/index.css +85 -15
  14. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  15. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  16. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  17. package/plugins/beta/datasets/src/DatasetsInit.jsx +23 -8
  18. package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
  19. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +113 -0
  20. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +69 -0
  21. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +338 -0
  22. package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +48 -0
  23. package/plugins/beta/datasets/src/api/addDataset.js +2 -8
  24. package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
  25. package/plugins/beta/datasets/src/api/getStyle.js +13 -0
  26. package/plugins/beta/datasets/src/api/removeDataset.js +2 -44
  27. package/plugins/beta/datasets/src/api/setData.js +8 -0
  28. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
  29. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
  30. package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
  31. package/plugins/beta/datasets/src/api/setStyle.js +22 -0
  32. package/plugins/beta/datasets/src/datasets.js +29 -55
  33. package/plugins/beta/datasets/src/defaults.js +42 -8
  34. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +34 -25
  35. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
  36. package/plugins/beta/datasets/src/manifest.js +24 -16
  37. package/plugins/beta/datasets/src/panels/Key.jsx +128 -50
  38. package/plugins/beta/datasets/src/panels/Key.module.scss +48 -9
  39. package/plugins/beta/datasets/src/panels/Layers.jsx +132 -29
  40. package/plugins/beta/datasets/src/panels/Layers.module.scss +50 -8
  41. package/plugins/beta/datasets/src/reducer.js +128 -9
  42. package/plugins/beta/datasets/src/styles/patterns.js +157 -0
  43. package/plugins/beta/datasets/src/utils/bbox.js +7 -5
  44. package/plugins/beta/datasets/src/utils/filters.js +5 -2
  45. package/plugins/beta/datasets/src/utils/mergeSublayer.js +78 -0
  46. package/plugins/beta/draw-ml/dist/css/index.css +1 -1
  47. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  48. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  49. package/plugins/beta/draw-ml/src/draw.scss +0 -7
  50. package/plugins/beta/draw-ml/src/manifest.js +16 -16
  51. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  52. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  53. package/plugins/beta/frame/src/Frame.jsx +5 -5
  54. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  55. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  56. package/plugins/beta/map-styles/src/manifest.js +1 -1
  57. package/plugins/beta/scale-bar/dist/css/index.css +1 -1
  58. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
  59. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
  60. package/plugins/beta/scale-bar/src/index.test.js +3 -3
  61. package/plugins/beta/scale-bar/src/manifest.js +3 -3
  62. package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
  63. package/plugins/interact/dist/css/index.css +1 -1
  64. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  65. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  66. package/plugins/interact/src/interact.scss +0 -7
  67. package/plugins/interact/src/manifest.js +14 -18
  68. package/plugins/interact/src/manifest.test.js +3 -1
  69. package/plugins/search/dist/css/index.css +1 -1
  70. package/plugins/search/src/components/Form/Form.module.scss +2 -1
  71. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  72. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  73. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  74. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  75. package/providers/maplibre/src/utils/highlightFeatures.js +1 -0
  76. package/providers/maplibre/src/utils/highlightFeatures.test.js +1 -0
  77. package/src/App/components/Actions/Actions.jsx +2 -2
  78. package/src/App/components/Actions/Actions.module.scss +0 -7
  79. package/src/App/components/Actions/Actions.test.jsx +1 -1
  80. package/src/App/components/Icon/Icon.jsx +3 -2
  81. package/src/App/components/Icon/Icon.module.scss +4 -0
  82. package/src/App/components/Icon/Icon.test.jsx +43 -4
  83. package/src/App/components/MapButton/MapButton.jsx +42 -17
  84. package/src/App/components/MapButton/MapButton.module.scss +4 -13
  85. package/src/App/components/MapButton/MapButton.test.jsx +27 -3
  86. package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
  87. package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
  88. package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
  89. package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
  90. package/src/App/hooks/useButtonStateEvaluator.js +12 -2
  91. package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
  92. package/src/App/hooks/useInterfaceAPI.js +6 -0
  93. package/src/App/hooks/useLayoutMeasurements.js +84 -18
  94. package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
  95. package/src/App/layout/Layout.jsx +12 -7
  96. package/src/App/layout/Layout.test.jsx +2 -2
  97. package/src/App/layout/layout.module.scss +67 -29
  98. package/src/App/registry/pluginRegistry.js +1 -1
  99. package/src/App/renderer/HtmlElementHost.jsx +2 -1
  100. package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
  101. package/src/App/renderer/mapButtons.js +1 -1
  102. package/src/App/renderer/mapPanels.test.js +2 -2
  103. package/src/App/renderer/slotHelpers.js +2 -2
  104. package/src/App/renderer/slotHelpers.test.js +5 -5
  105. package/src/App/renderer/slots.js +9 -5
  106. package/src/App/store/AppProvider.jsx +3 -1
  107. package/src/App/store/AppProvider.test.jsx +1 -1
  108. package/src/App/store/ServiceProvider.jsx +3 -1
  109. package/src/App/store/appActionsMap.js +16 -0
  110. package/src/App/store/appActionsMap.test.js +27 -0
  111. package/src/App/store/appDispatchMiddleware.js +1 -1
  112. package/src/App/store/appDispatchMiddleware.test.js +2 -2
  113. package/src/App/store/appReducer.js +2 -0
  114. package/src/InteractiveMap/InteractiveMap.js +4 -0
  115. package/src/config/appConfig.js +5 -2
  116. package/src/config/events.js +28 -0
  117. package/src/scss/main.scss +1 -0
  118. package/src/scss/settings/_dimensions.scss +0 -1
  119. package/src/utils/getSafeZoneInset.js +9 -7
  120. package/src/utils/getSafeZoneInset.test.js +10 -10
  121. package/webpack.dev.mjs +1 -1
  122. package/docs/api/slot-map.svg +0 -1
  123. package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
  124. package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
  125. package/plugins/beta/datasets/src/api/showDataset.js +0 -14
  126. package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
  127. package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
  128. package/plugins/beta/datasets/src/mapLayers.js +0 -164
  129. /package/src/{utils → services}/logger.js +0 -0
  130. /package/src/{utils → services}/logger.test.js +0 -0
@@ -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
+ }
@@ -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
+ }