@defra/interactive-map 0.0.16-alpha → 0.0.18-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 (224) 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/context.md +53 -7
  8. package/docs/api/map-style-config.md +41 -2
  9. package/docs/api/marker-config.md +53 -11
  10. package/docs/api/slots.md +16 -15
  11. package/docs/api/symbol-config.md +160 -0
  12. package/docs/api/symbol-registry.md +115 -0
  13. package/docs/api.md +25 -22
  14. package/docs/getting-started.md +4 -1
  15. package/docs/plugins/datasets.md +657 -0
  16. package/docs/plugins/interact.md +68 -43
  17. package/docs/plugins/search.md +15 -3
  18. package/docs/plugins.md +1 -1
  19. package/package.json +2 -2
  20. package/plugins/beta/datasets/dist/css/index.css +103 -15
  21. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  22. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  23. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  24. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  25. package/plugins/beta/datasets/src/DatasetsInit.jsx +29 -9
  26. package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
  27. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +159 -0
  28. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +75 -0
  29. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +440 -0
  30. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
  31. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
  32. package/plugins/beta/datasets/src/api/addDataset.js +2 -8
  33. package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
  34. package/plugins/beta/datasets/src/api/getStyle.js +13 -0
  35. package/plugins/beta/datasets/src/api/removeDataset.js +2 -44
  36. package/plugins/beta/datasets/src/api/setData.js +10 -0
  37. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
  38. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
  39. package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
  40. package/plugins/beta/datasets/src/api/setStyle.js +22 -0
  41. package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
  42. package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
  43. package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
  44. package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
  45. package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
  46. package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
  47. package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
  48. package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
  49. package/plugins/beta/datasets/src/datasets.js +39 -56
  50. package/plugins/beta/datasets/src/defaults.js +44 -8
  51. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +34 -25
  52. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
  53. package/plugins/beta/datasets/src/index.js +2 -1
  54. package/plugins/beta/datasets/src/manifest.js +25 -17
  55. package/plugins/beta/datasets/src/panels/Key.jsx +51 -51
  56. package/plugins/beta/datasets/src/panels/Key.module.scss +59 -9
  57. package/plugins/beta/datasets/src/panels/Layers.jsx +132 -29
  58. package/plugins/beta/datasets/src/panels/Layers.module.scss +56 -8
  59. package/plugins/beta/datasets/src/reducer.js +134 -9
  60. package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
  61. package/plugins/beta/datasets/src/utils/bbox.js +7 -5
  62. package/plugins/beta/datasets/src/utils/filters.js +5 -2
  63. package/plugins/beta/datasets/src/utils/mergeSublayer.js +86 -0
  64. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  65. package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
  66. package/plugins/beta/draw-ml/dist/css/index.css +21 -1
  67. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  68. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  69. package/plugins/beta/draw-ml/dist/umd/index.js +1 -1
  70. package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
  71. package/plugins/beta/draw-ml/src/draw.scss +0 -7
  72. package/plugins/beta/draw-ml/src/manifest.js +16 -16
  73. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  74. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  75. package/plugins/beta/frame/src/Frame.jsx +5 -5
  76. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  77. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  78. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  79. package/plugins/beta/map-styles/src/MapStyles.jsx +5 -4
  80. package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
  81. package/plugins/beta/map-styles/src/manifest.js +1 -1
  82. package/plugins/beta/scale-bar/dist/css/index.css +1 -1
  83. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
  84. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
  85. package/plugins/beta/scale-bar/src/index.test.js +3 -3
  86. package/plugins/beta/scale-bar/src/manifest.js +3 -3
  87. package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
  88. package/plugins/interact/dist/css/index.css +1 -1
  89. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  90. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  91. package/plugins/interact/dist/umd/index.js +1 -1
  92. package/plugins/interact/src/InteractInit.jsx +14 -5
  93. package/plugins/interact/src/InteractInit.test.js +26 -6
  94. package/plugins/interact/src/api/enable.test.js +7 -7
  95. package/plugins/interact/src/defaults.js +4 -6
  96. package/plugins/interact/src/events.js +9 -6
  97. package/plugins/interact/src/events.test.js +28 -4
  98. package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
  99. package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
  100. package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
  101. package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
  102. package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
  103. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
  104. package/plugins/interact/src/interact.scss +0 -7
  105. package/plugins/interact/src/manifest.js +14 -18
  106. package/plugins/interact/src/manifest.test.js +3 -1
  107. package/plugins/interact/src/reducer.js +23 -4
  108. package/plugins/interact/src/reducer.test.js +60 -11
  109. package/plugins/interact/src/utils/buildStylesMap.js +17 -4
  110. package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
  111. package/plugins/interact/src/utils/featureQueries.js +11 -6
  112. package/plugins/interact/src/utils/featureQueries.test.js +8 -1
  113. package/plugins/search/dist/css/index.css +1 -1
  114. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  115. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  116. package/plugins/search/src/Search.jsx +3 -1
  117. package/plugins/search/src/components/Form/Form.module.scss +2 -1
  118. package/plugins/search/src/events/fetchSuggestions.js +6 -4
  119. package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
  120. package/plugins/search/src/events/formHandlers.js +3 -3
  121. package/plugins/search/src/events/formHandlers.test.js +1 -1
  122. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  123. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  124. package/plugins/search/src/utils/updateMap.js +3 -3
  125. package/plugins/search/src/utils/updateMap.test.js +3 -3
  126. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  127. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  128. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  129. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  130. package/providers/maplibre/dist/umd/index.js +1 -1
  131. package/providers/maplibre/src/appEvents.js +7 -0
  132. package/providers/maplibre/src/appEvents.test.js +18 -4
  133. package/providers/maplibre/src/maplibreProvider.js +52 -0
  134. package/providers/maplibre/src/maplibreProvider.test.js +105 -1
  135. package/providers/maplibre/src/utils/highlightFeatures.js +37 -7
  136. package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -95
  137. package/providers/maplibre/src/utils/hoverCursor.js +61 -0
  138. package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
  139. package/providers/maplibre/src/utils/patternImages.js +70 -0
  140. package/providers/maplibre/src/utils/patternImages.test.js +180 -0
  141. package/providers/maplibre/src/utils/queryFeatures.js +38 -16
  142. package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
  143. package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
  144. package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
  145. package/providers/maplibre/src/utils/symbolImages.js +147 -0
  146. package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
  147. package/src/App/components/Actions/Actions.jsx +2 -2
  148. package/src/App/components/Actions/Actions.module.scss +0 -7
  149. package/src/App/components/Actions/Actions.test.jsx +1 -1
  150. package/src/App/components/Icon/Icon.jsx +3 -2
  151. package/src/App/components/Icon/Icon.module.scss +4 -0
  152. package/src/App/components/Icon/Icon.test.jsx +43 -4
  153. package/src/App/components/MapButton/MapButton.jsx +42 -17
  154. package/src/App/components/MapButton/MapButton.module.scss +4 -13
  155. package/src/App/components/MapButton/MapButton.test.jsx +27 -3
  156. package/src/App/components/Markers/Markers.jsx +122 -27
  157. package/src/App/components/Markers/Markers.module.scss +0 -10
  158. package/src/App/components/Markers/Markers.test.jsx +246 -0
  159. package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
  160. package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
  161. package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
  162. package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
  163. package/src/App/hooks/useButtonStateEvaluator.js +12 -2
  164. package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
  165. package/src/App/hooks/useInterfaceAPI.js +6 -0
  166. package/src/App/hooks/useInterfaceAPI.test.js +156 -0
  167. package/src/App/hooks/useLayoutMeasurements.js +84 -18
  168. package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
  169. package/src/App/hooks/useMarkersAPI.js +2 -5
  170. package/src/App/hooks/useMarkersAPI.test.js +4 -4
  171. package/src/App/layout/Layout.jsx +14 -9
  172. package/src/App/layout/Layout.test.jsx +6 -4
  173. package/src/App/layout/layout.module.scss +67 -29
  174. package/src/App/registry/pluginRegistry.js +1 -1
  175. package/src/App/renderer/HtmlElementHost.jsx +2 -1
  176. package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
  177. package/src/App/renderer/mapButtons.js +1 -1
  178. package/src/App/renderer/mapPanels.test.js +2 -2
  179. package/src/App/renderer/slotHelpers.js +2 -2
  180. package/src/App/renderer/slotHelpers.test.js +5 -5
  181. package/src/App/renderer/slots.js +9 -5
  182. package/src/App/store/AppProvider.jsx +3 -1
  183. package/src/App/store/AppProvider.test.jsx +1 -1
  184. package/src/App/store/ServiceProvider.jsx +8 -4
  185. package/src/App/store/appActionsMap.js +16 -0
  186. package/src/App/store/appActionsMap.test.js +27 -0
  187. package/src/App/store/appDispatchMiddleware.js +1 -1
  188. package/src/App/store/appDispatchMiddleware.test.js +2 -2
  189. package/src/App/store/appReducer.js +2 -0
  190. package/src/App/store/mapActionsMap.js +4 -6
  191. package/src/App/store/mapActionsMap.test.js +3 -2
  192. package/src/App/store/mapReducer.js +2 -1
  193. package/src/InteractiveMap/InteractiveMap.js +4 -0
  194. package/src/config/appConfig.js +5 -8
  195. package/src/config/appConfig.test.js +1 -2
  196. package/src/config/defaults.js +0 -2
  197. package/src/config/events.js +28 -0
  198. package/src/config/mapTheme.js +56 -0
  199. package/src/config/patternConfig.js +16 -0
  200. package/src/config/symbolConfig.js +80 -0
  201. package/src/scss/main.scss +1 -0
  202. package/src/scss/settings/_colors.scss +0 -9
  203. package/src/scss/settings/_dimensions.scss +0 -1
  204. package/src/services/patternRegistry.js +40 -0
  205. package/src/services/patternRegistry.test.js +48 -0
  206. package/src/services/symbolRegistry.js +113 -0
  207. package/src/services/symbolRegistry.test.js +262 -0
  208. package/src/types.js +93 -11
  209. package/src/utils/getSafeZoneInset.js +9 -7
  210. package/src/utils/getSafeZoneInset.test.js +10 -10
  211. package/src/utils/patternUtils.js +94 -0
  212. package/src/utils/patternUtils.test.js +160 -0
  213. package/src/utils/symbolUtils.js +85 -0
  214. package/src/utils/symbolUtils.test.js +156 -0
  215. package/webpack.dev.mjs +1 -1
  216. package/docs/api/slot-map.svg +0 -1
  217. package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
  218. package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
  219. package/plugins/beta/datasets/src/api/showDataset.js +0 -14
  220. package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
  221. package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
  222. package/plugins/beta/datasets/src/mapLayers.js +0 -164
  223. /package/src/{utils → services}/logger.js +0 -0
  224. /package/src/{utils → services}/logger.test.js +0 -0
@@ -0,0 +1,440 @@
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 { getPatternConfigs } from './patternImages.js'
5
+ import { getSymbolConfigs, getSymbolImageId } from './symbolImages.js'
6
+ import { mergeSublayer } from '../../utils/mergeSublayer.js'
7
+ import { scaleFactor } from '../../../../../../src/config/appConfig.js'
8
+
9
+ /**
10
+ * MapLibre GL JS implementation of the LayerAdapter interface for the datasets plugin.
11
+ *
12
+ * Owns all map-framework-specific concerns:
13
+ * - Source and layer creation (delegated to layerBuilders)
14
+ * - Pattern image registration (delegated to patternRegistry)
15
+ * - Visibility toggling, feature filtering, style changes
16
+ * - Style-change recovery (re-adding layers after basemap swap)
17
+ *
18
+ * Symbol image rasterisation is delegated to the map provider via
19
+ * `mapProvider.registerSymbols()`, keeping this adapter free of provider internals.
20
+ */
21
+ export default class MaplibreLayerAdapter {
22
+ /**
23
+ * @param {Object} mapProvider - Map provider instance (e.g. MapLibreProvider)
24
+ * @param {Object} symbolRegistry
25
+ * @param {Object} patternRegistry
26
+ */
27
+ constructor (mapProvider, symbolRegistry, patternRegistry) {
28
+ this._mapProvider = mapProvider
29
+ this._map = mapProvider.map
30
+ this._symbolRegistry = symbolRegistry
31
+ this._patternRegistry = patternRegistry
32
+ // datasetId → sourceId, used by setData to update the correct source
33
+ this._datasetSourceMap = new Map()
34
+ // Tracks all active symbol-type layer IDs so non-symbol layers can be kept below them
35
+ this._symbolLayerIds = new Set()
36
+ }
37
+
38
+ // ─── Lifecycle ──────────────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * Initialise all datasets: register patterns, add layers, then wait for idle.
42
+ * @param {Object[]} datasets
43
+ * @param {Object} mapStyle
44
+ * @returns {Promise<void>} Resolves once the map has processed all layers.
45
+ */
46
+ async init (datasets, mapStyle) {
47
+ const mapStyleId = mapStyle.id
48
+ const pixelRatio = this._pixelRatio
49
+ await Promise.all([
50
+ this._mapProvider.registerPatterns(getPatternConfigs(datasets, this._patternRegistry), mapStyleId, this._patternRegistry),
51
+ this._mapProvider.registerSymbols(getSymbolConfigs(datasets), mapStyle, this._symbolRegistry)
52
+ ])
53
+ this._symbolLayerIds.clear()
54
+ datasets.forEach(dataset => this._addLayers(dataset, mapStyle, pixelRatio))
55
+ await new Promise(resolve => this._map.once('idle', resolve))
56
+ }
57
+
58
+ /**
59
+ * Remove all layers and sources for the given datasets.
60
+ * @param {Object[]} datasets
61
+ */
62
+ destroy (datasets) {
63
+ const removedSourceIds = new Set()
64
+ datasets.forEach(dataset => {
65
+ const sourceId = getSourceId(dataset)
66
+ this._getLayersUsingSource(sourceId).forEach(layerId => {
67
+ if (this._map.getLayer(layerId)) {
68
+ this._map.removeLayer(layerId)
69
+ }
70
+ })
71
+ if (!removedSourceIds.has(sourceId) && this._map.getSource(sourceId)) {
72
+ this._map.removeSource(sourceId)
73
+ removedSourceIds.add(sourceId)
74
+ }
75
+ })
76
+ this._datasetSourceMap.clear()
77
+ }
78
+
79
+ /**
80
+ * Re-register patterns and re-add all layers after a basemap style change,
81
+ * then reapply cached dynamic source data and hidden-feature filters.
82
+ * @param {Object[]} datasets
83
+ * @param {Object} newMapStyle
84
+ * @param {Object} hiddenFeatures - pluginState.hiddenFeatures
85
+ * @param {Map} dynamicSources - datasetId → dynamic source instance
86
+ * @returns {Promise<void>}
87
+ */
88
+ async onStyleChange (datasets, newMapStyle, hiddenFeatures, dynamicSources) {
89
+ // MapLibre wipes all sources/layers on style change — must wait for idle first
90
+ await new Promise(resolve => this._map.once('idle', resolve))
91
+
92
+ const newStyleId = newMapStyle.id
93
+ const pixelRatio = this._pixelRatio
94
+ await Promise.all([
95
+ this._mapProvider.registerPatterns(getPatternConfigs(datasets, this._patternRegistry), newStyleId, this._patternRegistry),
96
+ this._mapProvider.registerSymbols(getSymbolConfigs(datasets), newMapStyle, this._symbolRegistry)
97
+ ])
98
+ this._symbolLayerIds.clear()
99
+ datasets.forEach(dataset => this._addLayers(dataset, newMapStyle, pixelRatio))
100
+
101
+ // Re-push cached data for dynamic sources
102
+ dynamicSources.forEach(source => source.reapply())
103
+
104
+ // Reapply hidden feature filters
105
+ Object.entries(hiddenFeatures).forEach(([datasetId, { idProperty, ids }]) => {
106
+ const dataset = datasets.find(d => d.id === datasetId)
107
+ if (!dataset) {
108
+ return
109
+ }
110
+ this._applyFeatureFilter(dataset, idProperty, ids)
111
+ })
112
+ }
113
+
114
+ /**
115
+ * Re-register symbols at the new pixel ratio and update icon-image on all symbol layers.
116
+ * Called when the map size changes so symbols are rasterised at the correct resolution.
117
+ * @param {Object[]} datasets
118
+ * @param {Object} mapStyle
119
+ * @returns {Promise<void>}
120
+ */
121
+ async onSizeChange (datasets, mapStyle) {
122
+ await this._mapProvider.registerSymbols(getSymbolConfigs(datasets), mapStyle, this._symbolRegistry)
123
+ const pixelRatio = this._pixelRatio
124
+ datasets.forEach(dataset => {
125
+ getAllLayerIds(dataset).forEach(layerId => {
126
+ if (!this._symbolLayerIds.has(layerId) || !this._map.getLayer(layerId)) { return }
127
+ const imageId = getSymbolImageId(dataset, mapStyle, this._symbolRegistry, false, pixelRatio)
128
+ if (imageId) {
129
+ this._map.setLayoutProperty(layerId, 'icon-image', imageId)
130
+ }
131
+ })
132
+ dataset.sublayers?.forEach(sublayer => {
133
+ const { symbolLayerId } = getSublayerLayerIds(dataset.id, sublayer.id)
134
+ if (!this._map.getLayer(symbolLayerId)) { return }
135
+ const merged = mergeSublayer(dataset, sublayer)
136
+ const imageId = getSymbolImageId(merged, mapStyle, this._symbolRegistry, false, pixelRatio)
137
+ if (imageId) {
138
+ this._map.setLayoutProperty(symbolLayerId, 'icon-image', imageId)
139
+ }
140
+ })
141
+ })
142
+ }
143
+
144
+ // ─── Dataset operations ─────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * Add a single dataset's source and layers to the map.
148
+ * @param {Object} dataset
149
+ * @param {Object} mapStyle
150
+ */
151
+ addDataset (dataset, mapStyle) {
152
+ this._addLayers(dataset, mapStyle, this._pixelRatio)
153
+ }
154
+
155
+ /**
156
+ * Remove a dataset's layers and source from the map.
157
+ * Shared sources (same tiles URL or geojson URL used by multiple datasets) are
158
+ * only removed when no other remaining dataset references them.
159
+ * @param {Object} dataset
160
+ * @param {Object[]} allDatasets - Full current dataset list, for shared-source check.
161
+ */
162
+ removeDataset (dataset, allDatasets) {
163
+ const sourceId = getSourceId(dataset)
164
+
165
+ getAllLayerIds(dataset).forEach(layerId => {
166
+ if (this._map.getLayer(layerId)) {
167
+ this._map.removeLayer(layerId)
168
+ }
169
+ this._symbolLayerIds.delete(layerId)
170
+ })
171
+
172
+ const sourceIsShared = allDatasets.some(d => d.id !== dataset.id && getSourceId(d) === sourceId)
173
+
174
+ if (!sourceIsShared && this._map.getSource(sourceId)) {
175
+ this._map.removeSource(sourceId)
176
+ }
177
+
178
+ this._datasetSourceMap.delete(dataset.id)
179
+ }
180
+
181
+ /**
182
+ * Make a dataset's layers visible.
183
+ * @param {string} datasetId
184
+ */
185
+ showDataset (datasetId) {
186
+ this._setDatasetVisibility(datasetId, 'visible')
187
+ }
188
+
189
+ /**
190
+ * Hide a dataset's layers.
191
+ * @param {string} datasetId
192
+ */
193
+ hideDataset (datasetId) {
194
+ this._setDatasetVisibility(datasetId, 'none')
195
+ }
196
+
197
+ /**
198
+ * Make a single sublayer's layers visible.
199
+ * @param {string} datasetId
200
+ * @param {string} sublayerId
201
+ */
202
+ showSublayer (datasetId, sublayerId) {
203
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getSublayerLayerIds(datasetId, sublayerId)
204
+ ;[fillLayerId, strokeLayerId, symbolLayerId].forEach(layerId => {
205
+ if (this._map.getLayer(layerId)) {
206
+ this._map.setLayoutProperty(layerId, 'visibility', 'visible')
207
+ }
208
+ })
209
+ }
210
+
211
+ /**
212
+ * Hide a single sublayer's layers.
213
+ * @param {string} datasetId
214
+ * @param {string} sublayerId
215
+ */
216
+ hideSublayer (datasetId, sublayerId) {
217
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getSublayerLayerIds(datasetId, sublayerId)
218
+ ;[fillLayerId, strokeLayerId, symbolLayerId].forEach(layerId => {
219
+ if (this._map.getLayer(layerId)) {
220
+ this._map.setLayoutProperty(layerId, 'visibility', 'none')
221
+ }
222
+ })
223
+ }
224
+
225
+ // ─── Feature operations ─────────────────────────────────────────────────────
226
+
227
+ /**
228
+ * Show previously hidden features by updating the layer exclusion filter.
229
+ * @param {Object} dataset
230
+ * @param {string} idProperty
231
+ * @param {Array} remainingHiddenIds - IDs that should remain hidden after this call.
232
+ */
233
+ showFeatures (dataset, idProperty, remainingHiddenIds) {
234
+ this._applyFeatureFilter(dataset, idProperty, remainingHiddenIds)
235
+ }
236
+
237
+ /**
238
+ * Hide features by updating the layer exclusion filter.
239
+ * @param {Object} dataset
240
+ * @param {string} idProperty
241
+ * @param {Array} allHiddenIds - Full set of IDs to hide (existing + new).
242
+ */
243
+ hideFeatures (dataset, idProperty, allHiddenIds) {
244
+ this._applyFeatureFilter(dataset, idProperty, allHiddenIds)
245
+ }
246
+
247
+ /**
248
+ * Update a dataset's style and re-render all its layers.
249
+ * @param {Object} dataset - Updated dataset (style changes already merged in)
250
+ * @param {Object} mapStyle
251
+ * @returns {Promise<void>}
252
+ */
253
+ async setStyle (dataset, mapStyle) {
254
+ const mapStyleId = mapStyle.id
255
+ const pixelRatio = this._pixelRatio
256
+ getAllLayerIds(dataset).forEach(layerId => {
257
+ if (this._map.getLayer(layerId)) {
258
+ this._map.removeLayer(layerId)
259
+ }
260
+ this._symbolLayerIds.delete(layerId)
261
+ })
262
+ await Promise.all([
263
+ this._mapProvider.registerPatterns(getPatternConfigs([dataset], this._patternRegistry), mapStyleId, this._patternRegistry),
264
+ this._mapProvider.registerSymbols(getSymbolConfigs([dataset]), mapStyle, this._symbolRegistry)
265
+ ])
266
+ this._addLayers(dataset, mapStyle, pixelRatio)
267
+ }
268
+
269
+ /**
270
+ * Update a single sublayer's style and re-render its layers.
271
+ * @param {Object} dataset - Updated dataset (sublayer style changes already merged in)
272
+ * @param {string} sublayerId
273
+ * @param {Object} mapStyle
274
+ * @returns {Promise<void>}
275
+ */
276
+ async setSublayerStyle (dataset, sublayerId, mapStyle) {
277
+ const mapStyleId = mapStyle.id
278
+ const pixelRatio = this._pixelRatio
279
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getSublayerLayerIds(dataset.id, sublayerId)
280
+ ;[fillLayerId, strokeLayerId, symbolLayerId].forEach(layerId => {
281
+ if (this._map.getLayer(layerId)) {
282
+ this._map.removeLayer(layerId)
283
+ }
284
+ this._symbolLayerIds.delete(layerId)
285
+ })
286
+ const sublayer = dataset.sublayers?.find(s => s.id === sublayerId)
287
+ if (!sublayer) {
288
+ return
289
+ }
290
+ await Promise.all([
291
+ this._mapProvider.registerPatterns(getPatternConfigs([dataset], this._patternRegistry), mapStyleId, this._patternRegistry),
292
+ this._mapProvider.registerSymbols(getSymbolConfigs([dataset]), mapStyle, this._symbolRegistry)
293
+ ])
294
+ const sourceId = this._datasetSourceMap.get(dataset.id)
295
+ const sourceLayer = dataset.tiles?.length ? dataset.sourceLayer : undefined
296
+ addSublayerLayers(this._map, dataset, sublayer, sourceId, sourceLayer, { mapStyle, symbolRegistry: this._symbolRegistry, patternRegistry: this._patternRegistry, pixelRatio })
297
+ this._maintainSymbolOrdering(dataset)
298
+ }
299
+
300
+ /**
301
+ * Set opacity for all layers belonging to a dataset.
302
+ * Uses setPaintProperty directly — safe to call on every slider tick.
303
+ * @param {string} datasetId
304
+ * @param {number} opacity
305
+ */
306
+ setOpacity (datasetId, opacity) {
307
+ const style = this._map.getStyle()
308
+ if (!style?.layers) {
309
+ return
310
+ }
311
+ style.layers
312
+ .filter(layer => layer.id === datasetId || layer.id.startsWith(`${datasetId}-`))
313
+ .forEach(layer => this._setPaintOpacity(layer.id, opacity))
314
+ }
315
+
316
+ /**
317
+ * Set opacity for a single sublayer's fill and stroke layers.
318
+ * Uses setPaintProperty directly — safe to call on every slider tick.
319
+ * @param {string} datasetId
320
+ * @param {string} sublayerId
321
+ * @param {number} opacity
322
+ */
323
+ setSublayerOpacity (datasetId, sublayerId, opacity) {
324
+ const { fillLayerId, strokeLayerId } = getSublayerLayerIds(datasetId, sublayerId)
325
+ this._setPaintOpacity(fillLayerId, opacity)
326
+ this._setPaintOpacity(strokeLayerId, opacity)
327
+ }
328
+
329
+ /**
330
+ * Update the GeoJSON data for a dataset's source.
331
+ * @param {string} datasetId
332
+ * @param {Object} geojson - GeoJSON FeatureCollection
333
+ */
334
+ setData (datasetId, geojson) {
335
+ const sourceId = this._datasetSourceMap.get(datasetId)
336
+ if (!sourceId) {
337
+ return
338
+ }
339
+ const source = this._map.getSource(sourceId)
340
+ if (source && typeof source.setData === 'function') {
341
+ source.setData(geojson)
342
+ }
343
+ }
344
+
345
+ // ─── Private ─────────────────────────────────────────────────────────────────
346
+
347
+ get _pixelRatio () {
348
+ return this._mapProvider.map.getPixelRatio() * (scaleFactor[this._mapProvider.mapSize] || 1)
349
+ }
350
+
351
+ _addLayers (dataset, mapStyle, pixelRatio) {
352
+ const sourceId = addDatasetLayers(this._map, dataset, mapStyle, this._symbolRegistry, this._patternRegistry, pixelRatio)
353
+ this._datasetSourceMap.set(dataset.id, sourceId)
354
+ this._maintainSymbolOrdering(dataset)
355
+ }
356
+
357
+ _getFirstSymbolLayerId () {
358
+ const style = this._map.getStyle()
359
+ if (!style?.layers) {
360
+ return null
361
+ }
362
+ const layer = style.layers.find(l => this._symbolLayerIds.has(l.id))
363
+ return layer?.id ?? null
364
+ }
365
+
366
+ _maintainSymbolOrdering (dataset) {
367
+ const layerIds = getAllLayerIds(dataset).filter(id => id && this._map.getLayer(id))
368
+ layerIds.forEach(id => {
369
+ if (this._map.getLayer(id)?.type === 'symbol') {
370
+ this._symbolLayerIds.add(id)
371
+ } else {
372
+ this._symbolLayerIds.delete(id)
373
+ }
374
+ })
375
+ const firstSymbolId = this._getFirstSymbolLayerId()
376
+ if (!firstSymbolId) {
377
+ return
378
+ }
379
+ layerIds.forEach(id => {
380
+ if (!this._symbolLayerIds.has(id)) {
381
+ this._map.moveLayer(id, firstSymbolId)
382
+ }
383
+ })
384
+ }
385
+
386
+ _setDatasetVisibility (datasetId, visibility) {
387
+ const style = this._map.getStyle()
388
+ if (!style?.layers) {
389
+ return
390
+ }
391
+ // Covers base fill layer (datasetId) and all suffixed layers
392
+ // (-stroke, -${sublayerId}, -${sublayerId}-stroke) without needing the dataset object.
393
+ style.layers
394
+ .filter(layer =>
395
+ layer.id === datasetId ||
396
+ layer.id.startsWith(`${datasetId}-`)
397
+ )
398
+ .forEach(layer => this._map.setLayoutProperty(layer.id, 'visibility', visibility))
399
+ }
400
+
401
+ _applyFeatureFilter (dataset, idProperty, excludeIds) {
402
+ if (dataset.sublayers?.length) {
403
+ dataset.sublayers.forEach(sublayer => {
404
+ const { fillLayerId: subFillId, strokeLayerId: subStrokeId } = getSublayerLayerIds(dataset.id, sublayer.id)
405
+ const sublayerFilter = dataset.filter && sublayer.filter
406
+ ? ['all', dataset.filter, sublayer.filter]
407
+ : (sublayer.filter || dataset.filter || null)
408
+ applyExclusionFilter(this._map, subFillId, sublayerFilter, idProperty, excludeIds)
409
+ applyExclusionFilter(this._map, subStrokeId, sublayerFilter, idProperty, excludeIds)
410
+ })
411
+ return
412
+ }
413
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getLayerIds(dataset)
414
+ const originalFilter = dataset.filter || null
415
+ const layerIds = [fillLayerId, strokeLayerId, symbolLayerId].filter(Boolean)
416
+ layerIds.forEach(layerId => {
417
+ applyExclusionFilter(this._map, layerId, originalFilter, idProperty, excludeIds)
418
+ })
419
+ }
420
+
421
+ _setPaintOpacity (layerId, opacity) {
422
+ const layer = this._map.getLayer(layerId)
423
+ if (!layer) {
424
+ return
425
+ }
426
+ const opacityProps = { line: 'line-opacity', symbol: 'icon-opacity' }
427
+ const prop = opacityProps[layer.type] || 'fill-opacity'
428
+ this._map.setPaintProperty(layerId, prop, opacity)
429
+ }
430
+
431
+ _getLayersUsingSource (sourceId) {
432
+ const style = this._map.getStyle()
433
+ if (!style?.layers) {
434
+ return []
435
+ }
436
+ return style.layers
437
+ .filter(layer => layer.source === sourceId)
438
+ .map(layer => layer.id)
439
+ }
440
+ }
@@ -0,0 +1,27 @@
1
+ // Re-export pure pattern utilities from core — no map engine dependency.
2
+ import { hasPattern } from '../../../../../../src/utils/patternUtils.js'
3
+ import { mergeSublayer } from '../../utils/mergeSublayer.js'
4
+
5
+ export { hasPattern, getPatternInnerContent, getPatternImageId, getKeyPatternPaths, injectColors } from '../../../../../../src/utils/patternUtils.js'
6
+
7
+ /**
8
+ * Returns a flat list of datasets and merged sublayers that require pattern images.
9
+ * Handles sublayer merging so callers (e.g. mapProvider.registerPatterns) receive ready-to-use configs.
10
+ *
11
+ * @param {Object[]} datasets
12
+ * @param {Object} patternRegistry
13
+ * @returns {Object[]}
14
+ */
15
+ export const getPatternConfigs = (datasets, patternRegistry) =>
16
+ datasets.flatMap(dataset => {
17
+ const configs = hasPattern(dataset) ? [dataset] : []
18
+ if (dataset.sublayers?.length) {
19
+ dataset.sublayers.forEach(sublayer => {
20
+ const merged = mergeSublayer(dataset, sublayer)
21
+ if (hasPattern(merged)) {
22
+ configs.push(merged)
23
+ }
24
+ })
25
+ }
26
+ return configs
27
+ })
@@ -0,0 +1,31 @@
1
+ // Re-export pure symbol resolution utilities from core — no map engine dependency.
2
+ import { hasSymbol } from '../../../../../../src/utils/symbolUtils.js'
3
+ import { mergeSublayer } from '../../utils/mergeSublayer.js'
4
+
5
+ export { hasSymbol, getSymbolDef, getSymbolStyleColors, getSymbolViewBox, getSymbolAnchor } from '../../../../../../src/utils/symbolUtils.js'
6
+
7
+ // Re-export MapLibre-specific symbol utilities from the provider.
8
+ // This is the single cross-boundary import in the adapter; in a separate-package
9
+ // setup this would be: '@interactive-map/maplibre-provider/utils/symbolImages'
10
+ export { anchorToMaplibre, getSymbolImageId } from '../../../../../../providers/maplibre/src/utils/symbolImages.js'
11
+
12
+ /**
13
+ * Returns a flat list of datasets and merged sublayers that require symbol images.
14
+ * Handles sublayer merging so callers (e.g. mapProvider.registerSymbols) receive ready-to-use configs.
15
+ *
16
+ * @param {Object[]} datasets
17
+ * @returns {Object[]}
18
+ */
19
+ export const getSymbolConfigs = (datasets) =>
20
+ datasets.flatMap(dataset => {
21
+ const configs = hasSymbol(dataset) ? [dataset] : []
22
+ if (dataset.sublayers?.length) {
23
+ dataset.sublayers.forEach(sublayer => {
24
+ const merged = mergeSublayer(dataset, sublayer)
25
+ if (hasSymbol(merged)) {
26
+ configs.push(merged)
27
+ }
28
+ })
29
+ }
30
+ return configs
31
+ })
@@ -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)
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,10 @@
1
+ import { logger } from '../../../../../src/services/logger.js'
2
+
3
+ export const setData = ({ pluginState }, geojson, { datasetId }) => {
4
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
5
+ if (dataset?.tiles) {
6
+ logger.warn(`setData called on vector tile dataset "${datasetId}" — has no effect`)
7
+ return
8
+ }
9
+ pluginState.layerAdapter?.setData(datasetId, geojson)
10
+ }
@@ -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
+ }