@defra/interactive-map 0.0.50-alpha → 0.0.51-apha

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 (76) hide show
  1. package/dist/esm/im-core.js +1 -1
  2. package/dist/umd/im-core.js +1 -1
  3. package/docs/api.md +19 -0
  4. package/docs/architecture.md +14 -0
  5. package/docs/getting-started.md +10 -0
  6. package/docs/plugins/datasets.md +9 -3
  7. package/jest.setup.js +8 -0
  8. package/package.json +8 -3
  9. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  10. package/plugins/datasets/dist/esm/im-datasets-ol-adapter.js +1 -0
  11. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  12. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  13. package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js +2 -0
  14. package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js.LICENSE.txt +1 -0
  15. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  16. package/plugins/datasets/dist/umd/index.js +1 -1
  17. package/plugins/datasets/src/adapters/loadLayerAdapter.js +7 -3
  18. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +17 -0
  19. package/plugins/datasets/src/adapters/mapboxStyleLayerAdapter.js +74 -0
  20. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +14 -79
  21. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +4 -0
  22. package/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +3 -89
  23. package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.js +111 -0
  24. package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.test.js +198 -0
  25. package/plugins/datasets/src/adapters/openlayers/layerBuilders.js +205 -0
  26. package/plugins/datasets/src/adapters/openlayers/layerBuilders.test.js +233 -0
  27. package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.js +276 -0
  28. package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.test.js +431 -0
  29. package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.js +172 -0
  30. package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.test.js +262 -0
  31. package/plugins/datasets/src/fetch/createDynamicSource.js +12 -8
  32. package/plugins/datasets/src/fetch/createDynamicSource.test.js +61 -52
  33. package/plugins/datasets/src/initialise/DatasetsInit.jsx +13 -1
  34. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +20 -0
  35. package/plugins/datasets/src/initialise/initialiseDatasets.js +1 -1
  36. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +1 -1
  37. package/plugins/datasets/src/registry/mapboxStyleDataset.js +108 -0
  38. package/plugins/datasets/src/registry/mapboxStyleDataset.test.js +240 -0
  39. package/plugins/datasets/src/utils/bbox.js +5 -7
  40. package/plugins/datasets/src/utils/bbox.test.js +3 -10
  41. package/plugins/draw/dist/css/index.css +1 -0
  42. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  43. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  44. package/plugins/draw/src/index.js +2 -0
  45. package/plugins/draw/src/index.test.js +1 -0
  46. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
  47. package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
  48. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  49. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  50. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  51. package/providers/beta/openlayers/src/index.js +16 -0
  52. package/providers/beta/openlayers/src/openlayersProvider.js +160 -7
  53. package/providers/beta/openlayers/src/openlayersProvider.test.js +13 -0
  54. package/providers/beta/openlayers/src/utils/bngProjection.js +9 -0
  55. package/providers/beta/openlayers/src/utils/filterEvaluator.js +32 -0
  56. package/providers/beta/openlayers/src/utils/filterEvaluator.test.js +36 -0
  57. package/providers/beta/openlayers/src/utils/highlightFeatures.js +38 -23
  58. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +153 -8
  59. package/providers/beta/openlayers/src/utils/hoverCursor.js +5 -1
  60. package/providers/beta/openlayers/src/utils/hoverCursor.test.js +40 -0
  61. package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -54
  62. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +143 -1
  63. package/providers/beta/openlayers/src/utils/symbolImages.js +79 -5
  64. package/providers/beta/openlayers/src/utils/symbolImages.test.js +111 -1
  65. package/providers/beta/openlayers/src/utils/vtTileFragments.js +20 -8
  66. package/providers/beta/openlayers/src/utils/vtTileFragments.test.js +162 -0
  67. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  68. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  69. package/providers/maplibre/dist/umd/index.js +1 -1
  70. package/providers/maplibre/src/utils/patternImages.js +4 -44
  71. package/rollup.esm.mjs +2 -0
  72. package/src/services/patternRegistry.js +38 -0
  73. package/src/services/patternRegistry.test.js +61 -0
  74. package/src/types.js +11 -0
  75. package/webpack.dev.mjs +1 -0
  76. /package/plugins/datasets/dist/esm/{esriLayerAdapter.js → im-datasets-esri-adapter.js} +0 -0
@@ -0,0 +1,276 @@
1
+ import { OpenLayersDataset } from './registry/openLayersDataset.js'
2
+ import { datasetRegistry } from '../../registry/datasetRegistry.js'
3
+ import { MapboxStyleLayerAdapter } from '../mapboxStyleLayerAdapter.js'
4
+ import { createDatasetSource, createDatasetLayer, resolveLayerStyle, readGeoJSONFeatures } from './layerBuilders.js'
5
+ import { registerSymbols, SYMBOL_RASTER_PIXEL_RATIO } from '../../../../../providers/beta/openlayers/src/utils/symbolImages.js'
6
+ import { registerCrispCanvasPatterns } from './canvasPatternStyle.js'
7
+ import { logger } from '../../../../../src/services/logger.js'
8
+
9
+ /**
10
+ * OpenLayers implementation of the LayerAdapter interface for the datasets plugin.
11
+ *
12
+ * GeoJSON and vector tile datasets are both supported (see layerBuilders.createDatasetSource).
13
+ * Pattern fills and symbols are both wired into OpenLayersDataset.flatStyle, registered ahead
14
+ * of time here (see _registerPatterns/_registerSymbols) since flatStyle is a synchronous getter.
15
+ *
16
+ * One OL layer per dataset/sublayer (see OpenLayersDataset's leafLayerIds), not up to three like
17
+ * MapLibre — an ol/style/flat spec can combine fill+stroke (or an icon) for one feature in a
18
+ * single layer. Sublayers of the same parent share one OL source (tracked by sourceId), each
19
+ * with its own layer + filter-scoped style, mirroring how MapLibre sublayers share one source.
20
+ */
21
+ export default class OpenLayersLayerAdapter extends MapboxStyleLayerAdapter {
22
+ /**
23
+ * @param {Object} mapProvider - Map provider instance (e.g. OpenLayersProvider)
24
+ * @param {Object} symbolRegistry
25
+ * @param {Object} patternRegistry
26
+ */
27
+ constructor (mapProvider, symbolRegistry, patternRegistry) {
28
+ super(mapProvider, symbolRegistry, patternRegistry)
29
+ // sourceId → OL source, shared across sublayers of the same dataset
30
+ this._sourcesById = new Map()
31
+ // leaf layer id (== dataset/sublayer id) → OL layer
32
+ this._layersById = new Map()
33
+ // initialiseDatasets.js passes this to datasetRegistry.attachCreateDataset unbound, then
34
+ // calls it as registry._createDataset(...) — bind so `this` still resolves here, not to
35
+ // the registry.
36
+ this.createDataset = this.createDataset.bind(this)
37
+ }
38
+
39
+ createDataset (datasetDefinition) {
40
+ return new OpenLayersDataset(datasetDefinition)
41
+ }
42
+
43
+ get _styleContext () {
44
+ return { mapStyleId: datasetRegistry.mapStyle?.id, pixelRatio: this._pixelRatio, patternRegistry: this._patternRegistry }
45
+ }
46
+
47
+ // ─── Lifecycle ──────────────────────────────────────────────────────────────
48
+
49
+ async init () {
50
+ await Promise.all([this._registerPatterns(), this._registerSymbols()])
51
+ datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset))
52
+ }
53
+
54
+ destroy () {
55
+ this._layersById.forEach(layer => this._map.removeLayer(layer))
56
+ this._layersById.clear()
57
+ this._sourcesById.clear()
58
+ }
59
+
60
+ // OL's basemap swap (see providers/beta/openlayers's appEvents.js) only replaces the base
61
+ // tile layer — dataset layers are separate map.addLayer() entries and are never wiped, so
62
+ // unlike MapLibre there's nothing to rebuild here, just colour tokens to re-resolve. Pattern
63
+ // fg/bg colours can be mapStyle-keyed too, so re-registering (not just re-styling) is needed.
64
+ async onMapStyleChange () {
65
+ await Promise.all([this._registerPatterns(), this._registerSymbols()])
66
+ datasetRegistry.forEachDataset(registryDataset => this._forEachLeafDataset(registryDataset, leaf => {
67
+ this._setLayerStyle(leaf)
68
+ }))
69
+ }
70
+
71
+ // Plain fill-color/stroke layers don't depend on map size at all. But a pattern fill (see
72
+ // canvasPatternStyle.js) is deliberately rasterised at the map's *actual* pixelRatio to stay
73
+ // genuinely crisp, so a pixelRatio change (map resize) needs a fresh rasterisation and a
74
+ // re-applied style, the same way MapLibreLayerAdapter.onMapSizeChange re-rasterises
75
+ // symbols/patterns at the new ratio.
76
+ async onMapSizeChange () {
77
+ await this._registerPatterns()
78
+ datasetRegistry.forEachDataset(registryDataset => this._forEachLeafDataset(registryDataset, leaf => {
79
+ if (leaf.hasPattern) {
80
+ this._setLayerStyle(leaf)
81
+ }
82
+ }))
83
+ }
84
+
85
+ // ─── Dataset operations ─────────────────────────────────────────────────────
86
+ // addDataset is inherited from MapboxStyleLayerAdapter.
87
+
88
+ _registerPatternsAndSymbols (registryDataset) {
89
+ return Promise.all([this._registerPatterns([registryDataset]), this._registerSymbols([registryDataset])])
90
+ }
91
+
92
+ removeDataset (datasetId) {
93
+ const registryDataset = datasetRegistry.getDataset(datasetId)
94
+ if (!registryDataset) {
95
+ return
96
+ }
97
+ registryDataset.layerIds.forEach(layerId => {
98
+ const layer = this._layersById.get(layerId)
99
+ if (layer) {
100
+ this._map.removeLayer(layer)
101
+ this._layersById.delete(layerId)
102
+ }
103
+ })
104
+
105
+ const { sourceId } = registryDataset
106
+ const sourceIsShared = datasetRegistry.topLevelDatasets()
107
+ .some(dataset => dataset.id !== datasetId && dataset.sourceId === sourceId)
108
+ if (!sourceIsShared) {
109
+ this._sourcesById.delete(sourceId)
110
+ }
111
+ }
112
+
113
+ async applyStyle (datasetId) {
114
+ const registryDataset = datasetRegistry.getDataset(datasetId)
115
+ if (!registryDataset) {
116
+ return
117
+ }
118
+ // A style change can introduce a pattern/symbol that wasn't previously configured.
119
+ await this._registerPatternsAndSymbols(registryDataset)
120
+ this._forEachLeafDataset(registryDataset, leaf => {
121
+ if (this._layersById.has(leaf.id)) {
122
+ this._setLayerStyle(leaf)
123
+ } else {
124
+ this._addLayers(leaf)
125
+ }
126
+ })
127
+ }
128
+
129
+ // ─── Feature operations ─────────────────────────────────────────────────────
130
+ // Opacity/visibility methods are inherited from MapboxStyleLayerAdapter.
131
+
132
+ _setLayerOpacity (layerId, opacity) {
133
+ this._layersById.get(layerId)?.setOpacity(opacity)
134
+ }
135
+
136
+ _setLayerVisibility (layerId, visibility) {
137
+ this._layersById.get(layerId)?.setVisible(visibility === 'visible')
138
+ }
139
+
140
+ // Hidden features are baked into flatStyle's filter (see OpenLayersDataset), so applying the
141
+ // filter means recomputing and re-setting style rather than a separate declarative call like
142
+ // MapLibre's map.setFilter.
143
+ applyFeatureFilter (datasetId) {
144
+ const registryDataset = datasetRegistry.getDataset(datasetId)
145
+ if (!registryDataset) {
146
+ return
147
+ }
148
+ this._forEachLeafDataset(registryDataset, leaf => {
149
+ this._setLayerStyle(leaf)
150
+ })
151
+ }
152
+
153
+ setData (datasetId, geojson) {
154
+ const registryDataset = datasetRegistry.getDataset(datasetId)
155
+ const olSource = registryDataset && this._sourcesById.get(registryDataset.sourceId)
156
+ if (!olSource) {
157
+ return
158
+ }
159
+ olSource.clear()
160
+ if (geojson?.features?.length) {
161
+ olSource.addFeatures(readGeoJSONFeatures(geojson, registryDataset.idStrategy))
162
+ }
163
+ }
164
+
165
+ // ─── Private ─────────────────────────────────────────────────────────────────
166
+
167
+ // Rasterises and caches each pattern's image ahead of time, so OpenLayersDataset.flatStyle /
168
+ // resolveLayerStyle (both synchronous) can find it already resolved — mirrors
169
+ // MapLibreLayerAdapter's addPatternsAndSymbolsToMap, which likewise runs before _addLayers
170
+ // reads the dataset's style. Goes through canvasPatternStyle.js's genuinely-crisp,
171
+ // pixelRatio-scaled pipeline — see layerBuilders.js's resolveLayerStyle.
172
+ async _registerPatterns (registryDatasets = datasetRegistry.topLevelDatasets()) {
173
+ const styles = []
174
+ registryDatasets.forEach(registryDataset => this._forEachLeafDataset(registryDataset, leaf => {
175
+ if (leaf.hasPattern) {
176
+ styles.push(leaf.style)
177
+ }
178
+ }))
179
+ const mapStyleId = datasetRegistry.mapStyle?.id
180
+ await registerCrispCanvasPatterns(styles, mapStyleId, this._patternRegistry, this._pixelRatio)
181
+ }
182
+
183
+ // Rasterises and caches each symbol's icon image ahead of time, so OpenLayersDataset.flatStyle
184
+ // (a synchronous getter) can find it already resolved — same convention as _registerPatterns.
185
+ async _registerSymbols (registryDatasets = datasetRegistry.topLevelDatasets()) {
186
+ const styles = []
187
+ registryDatasets.forEach(registryDataset => this._forEachLeafDataset(registryDataset, leaf => {
188
+ if (leaf.hasSymbol) {
189
+ styles.push(leaf.style)
190
+ }
191
+ }))
192
+ await registerSymbols(styles, datasetRegistry.mapStyle, this._symbolRegistry, SYMBOL_RASTER_PIXEL_RATIO)
193
+ }
194
+
195
+ // ol/style/flat's expression parser is stricter than MapLibre's filter dialect in some places
196
+ // — e.g. its "in" operator requires a real array second argument, where MapLibre also accepts
197
+ // a bare scalar. A hand-authored dataset filter written for MapLibre can hit this; catch per
198
+ // dataset so one bad config can't take down every other dataset's layers.
199
+ _setLayerStyle (registryDataset) {
200
+ const layer = this._layersById.get(registryDataset.id)
201
+ if (!layer) {
202
+ return
203
+ }
204
+ try {
205
+ layer.setStyle(resolveLayerStyle(registryDataset, this._styleContext))
206
+ // symbolMeta.imageId is only ever resolved for the 'normal' variant, but that's still
207
+ // mapStyle-dependent (resolve() falls back to the scheme's own foreground/halo colours,
208
+ // not just resolveActive/resolveSelected) — so onMapStyleChange (which calls this after
209
+ // re-registering) needs this refreshed too, or highlightFeatures.js keeps reverse-mapping
210
+ // from a stale, pre-switch base id forever (see layerBuilders.js's createDatasetLayer,
211
+ // which sets this the same way when the layer is first built).
212
+ if (registryDataset.hasSymbol) {
213
+ layer.set('symbolMeta', registryDataset.symbolMeta)
214
+ }
215
+ } catch (error) {
216
+ logger.warn(`OpenLayers datasets adapter: failed to build a style for dataset "${registryDataset.id}" — ${error.message}`)
217
+ }
218
+ }
219
+
220
+ _forEachLeafDataset (registryDataset, callback) {
221
+ if (registryDataset.hasSublayers) {
222
+ registryDataset.sublayers.forEach(sublayer => this._forEachLeafDataset(sublayer, callback))
223
+ return
224
+ }
225
+ callback(registryDataset)
226
+ }
227
+
228
+ // Wrapped the same way _setLayerStyle/_addLayers wrap style/layer construction: a malformed
229
+ // dataset config (e.g. a bad tile URL) should log and skip that one dataset, not crash the
230
+ // whole init()/forEachDataset loop for every other dataset sharing it.
231
+ _getOrCreateSource (registryDataset) {
232
+ const { sourceId } = registryDataset
233
+ if (this._sourcesById.has(sourceId)) {
234
+ return this._sourcesById.get(sourceId)
235
+ }
236
+ let olSource
237
+ try {
238
+ olSource = createDatasetSource(registryDataset)
239
+ } catch (error) {
240
+ logger.warn(`OpenLayers datasets adapter: failed to build a source for dataset "${registryDataset.id}" — ${error.message}`)
241
+ return null
242
+ }
243
+ if (olSource) {
244
+ this._sourcesById.set(sourceId, olSource)
245
+ }
246
+ return olSource
247
+ }
248
+
249
+ _addLayers (registryDataset) {
250
+ if (registryDataset.hasSublayers) {
251
+ // A sublayer's own geojson/tiles is always undefined (Dataset.tiles/geojson don't
252
+ // inherit from parent) — the shared source can only be built from the parent's own
253
+ // source descriptor, so create/cache it here before any sublayer looks it up by sourceId.
254
+ this._getOrCreateSource(registryDataset)
255
+ registryDataset.sublayers.forEach(sublayer => this._addLayers(sublayer))
256
+ return
257
+ }
258
+ if (!registryDataset.leafLayerIds.length) {
259
+ return
260
+ }
261
+ const olSource = this._getOrCreateSource(registryDataset)
262
+ if (!olSource) {
263
+ return
264
+ }
265
+ let layer
266
+ try {
267
+ // See _setLayerStyle's comment: a flatStyle built from a hand-authored filter can throw.
268
+ layer = createDatasetLayer(registryDataset, olSource, this._styleContext)
269
+ } catch (error) {
270
+ logger.warn(`OpenLayers datasets adapter: failed to build a style for dataset "${registryDataset.id}" — ${error.message}`)
271
+ return
272
+ }
273
+ this._map.addLayer(layer)
274
+ this._layersById.set(registryDataset.id, layer)
275
+ }
276
+ }