@defra/interactive-map 0.0.49-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.
- package/dist/esm/im-core.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/docs/api.md +19 -0
- package/docs/architecture.md +14 -0
- package/docs/getting-started.md +10 -0
- package/docs/plugins/datasets.md +9 -3
- package/jest.setup.js +8 -0
- package/package.json +8 -3
- package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-ol-adapter.js +1 -0
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js +2 -0
- package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js.LICENSE.txt +1 -0
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/index.js +1 -1
- package/plugins/datasets/src/adapters/loadLayerAdapter.js +7 -3
- package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +17 -0
- package/plugins/datasets/src/adapters/mapboxStyleLayerAdapter.js +74 -0
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +14 -79
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +4 -0
- package/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +3 -89
- package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.js +111 -0
- package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.test.js +198 -0
- package/plugins/datasets/src/adapters/openlayers/layerBuilders.js +205 -0
- package/plugins/datasets/src/adapters/openlayers/layerBuilders.test.js +233 -0
- package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.js +276 -0
- package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.test.js +431 -0
- package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.js +172 -0
- package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.test.js +262 -0
- package/plugins/datasets/src/fetch/createDynamicSource.js +12 -8
- package/plugins/datasets/src/fetch/createDynamicSource.test.js +61 -52
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +13 -1
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +20 -0
- package/plugins/datasets/src/initialise/initialiseDatasets.js +1 -1
- package/plugins/datasets/src/initialise/initialiseDatasets.test.js +1 -1
- package/plugins/datasets/src/registry/mapboxStyleDataset.js +108 -0
- package/plugins/datasets/src/registry/mapboxStyleDataset.test.js +240 -0
- package/plugins/datasets/src/utils/bbox.js +5 -7
- package/plugins/datasets/src/utils/bbox.test.js +3 -10
- package/plugins/draw/dist/css/index.css +1 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/src/index.js +2 -0
- package/plugins/draw/src/index.test.js +1 -0
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/src/components/Form/Form.module.scss +10 -1
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/index.js +16 -0
- package/providers/beta/openlayers/src/openlayersProvider.js +160 -7
- package/providers/beta/openlayers/src/openlayersProvider.test.js +13 -0
- package/providers/beta/openlayers/src/utils/bngProjection.js +9 -0
- package/providers/beta/openlayers/src/utils/filterEvaluator.js +32 -0
- package/providers/beta/openlayers/src/utils/filterEvaluator.test.js +36 -0
- package/providers/beta/openlayers/src/utils/highlightFeatures.js +38 -23
- package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +153 -8
- package/providers/beta/openlayers/src/utils/hoverCursor.js +5 -1
- package/providers/beta/openlayers/src/utils/hoverCursor.test.js +40 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -54
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +143 -1
- package/providers/beta/openlayers/src/utils/symbolImages.js +79 -5
- package/providers/beta/openlayers/src/utils/symbolImages.test.js +111 -1
- package/providers/beta/openlayers/src/utils/vtTileFragments.js +20 -8
- package/providers/beta/openlayers/src/utils/vtTileFragments.test.js +162 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/utils/patternImages.js +4 -44
- package/rollup.esm.mjs +2 -0
- package/src/App/hooks/useLayoutMeasurements.js +1 -1
- package/src/App/hooks/useMapProviderOverrides.js +24 -8
- package/src/App/hooks/useMapProviderOverrides.test.js +11 -2
- package/src/services/patternRegistry.js +38 -0
- package/src/services/patternRegistry.test.js +61 -0
- package/src/types.js +11 -0
- package/src/utils/getSafeZoneInset.js +26 -62
- package/src/utils/getSafeZoneInset.test.js +36 -64
- package/webpack.dev.mjs +1 -0
- /package/plugins/datasets/dist/esm/{esriLayerAdapter.js → im-datasets-esri-adapter.js} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { LayerAdapter } from '../layerAdapter.js'
|
|
2
1
|
import { addDatasetLayers } from './layerBuilders.js'
|
|
3
2
|
import { MapLibreDataset } from './registry/mapLibreDataset.js'
|
|
4
3
|
import { datasetRegistry } from '../../registry/datasetRegistry.js'
|
|
4
|
+
import { MapboxStyleLayerAdapter } from '../mapboxStyleLayerAdapter.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* MapLibre GL JS implementation of the LayerAdapter interface for the datasets plugin.
|
|
@@ -15,18 +15,14 @@ import { datasetRegistry } from '../../registry/datasetRegistry.js'
|
|
|
15
15
|
* Symbol image rasterisation is delegated to the map provider via
|
|
16
16
|
* `mapProvider.addSymbolsToMap()`, keeping this adapter free of provider internals.
|
|
17
17
|
*/
|
|
18
|
-
export default class MaplibreLayerAdapter extends
|
|
18
|
+
export default class MaplibreLayerAdapter extends MapboxStyleLayerAdapter {
|
|
19
19
|
/**
|
|
20
20
|
* @param {Object} mapProvider - Map provider instance (e.g. MapLibreProvider)
|
|
21
21
|
* @param {Object} symbolRegistry
|
|
22
22
|
* @param {Object} patternRegistry
|
|
23
23
|
*/
|
|
24
24
|
constructor (mapProvider, symbolRegistry, patternRegistry) {
|
|
25
|
-
super()
|
|
26
|
-
this._mapProvider = mapProvider
|
|
27
|
-
this._map = mapProvider.map
|
|
28
|
-
this._symbolRegistry = symbolRegistry
|
|
29
|
-
this._patternRegistry = patternRegistry
|
|
25
|
+
super(mapProvider, symbolRegistry, patternRegistry)
|
|
30
26
|
// datasetId → sourceId, used by setData to update the correct source
|
|
31
27
|
this._datasetSourceMap = new Map()
|
|
32
28
|
window._datasetSourceMap = this._datasetSourceMap // Expose for debugging
|
|
@@ -137,14 +133,10 @@ export default class MaplibreLayerAdapter extends LayerAdapter {
|
|
|
137
133
|
|
|
138
134
|
// ─── Dataset operations ─────────────────────────────────────────────────────
|
|
139
135
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
async addDataset (datasetId) {
|
|
145
|
-
const registryDataset = datasetRegistry.getDataset(datasetId)
|
|
146
|
-
await this.addPatternsAndSymbolsToMap(registryDataset.patternConfigs, registryDataset.symbolConfigs)
|
|
147
|
-
this._addLayers(registryDataset)
|
|
136
|
+
// addDataset is inherited from MapboxStyleLayerAdapter, which now guards an unknown
|
|
137
|
+
// datasetId (previously this threw here, matching OpenLayersLayerAdapter's behaviour).
|
|
138
|
+
_registerPatternsAndSymbols (registryDataset) {
|
|
139
|
+
return this.addPatternsAndSymbolsToMap(registryDataset.patternConfigs, registryDataset.symbolConfigs)
|
|
148
140
|
}
|
|
149
141
|
|
|
150
142
|
/**
|
|
@@ -196,38 +188,11 @@ export default class MaplibreLayerAdapter extends LayerAdapter {
|
|
|
196
188
|
async applyStyle (datasetId) {
|
|
197
189
|
const registryDataset = datasetRegistry.getDataset(datasetId)
|
|
198
190
|
registryDataset.layerIds.forEach(layerId => this.removeLayer(layerId))
|
|
199
|
-
await this.
|
|
191
|
+
await this._registerPatternsAndSymbols(registryDataset)
|
|
200
192
|
this._addLayers(registryDataset)
|
|
201
193
|
}
|
|
202
194
|
|
|
203
|
-
|
|
204
|
-
* Apply opacity for all layers with datasetId
|
|
205
|
-
* @param {string} datasetId
|
|
206
|
-
*/
|
|
207
|
-
applyDatasetOpacity (datasetId) {
|
|
208
|
-
const registryDataset = datasetRegistry.getDataset(datasetId)
|
|
209
|
-
if (registryDataset) {
|
|
210
|
-
this._applyRegistryDatasetOpacity(registryDataset)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Apply opacity for all layers belonging to a registryDataset.
|
|
216
|
-
* Uses setPaintProperty directly — safe to call on every slider tick.
|
|
217
|
-
* @param {Object} registryDataset
|
|
218
|
-
*/
|
|
219
|
-
_applyRegistryDatasetOpacity (registryDataset) {
|
|
220
|
-
registryDataset.getLayersWithOpacity().forEach(({ layerIds, opacity }) => {
|
|
221
|
-
layerIds.forEach(layerId => this._setPaintOpacity(layerId, opacity))
|
|
222
|
-
})
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Apply opacity for all layers
|
|
227
|
-
*/
|
|
228
|
-
applyGlobalOpacity () {
|
|
229
|
-
datasetRegistry.forEachDataset(registryDataset => this._applyRegistryDatasetOpacity(registryDataset))
|
|
230
|
-
}
|
|
195
|
+
// Opacity/visibility methods are inherited from MapboxStyleLayerAdapter.
|
|
231
196
|
|
|
232
197
|
/**
|
|
233
198
|
* Update the GeoJSON data for a dataset's source.
|
|
@@ -247,10 +212,6 @@ export default class MaplibreLayerAdapter extends LayerAdapter {
|
|
|
247
212
|
|
|
248
213
|
// ─── Private ─────────────────────────────────────────────────────────────────
|
|
249
214
|
|
|
250
|
-
get _pixelRatio () {
|
|
251
|
-
return this._mapProvider.map.getPixelRatio()
|
|
252
|
-
}
|
|
253
|
-
|
|
254
215
|
_addLayers (registryDataset) {
|
|
255
216
|
const { mapStyle } = datasetRegistry
|
|
256
217
|
const sourceId = addDatasetLayers(this._map, registryDataset, mapStyle, this._symbolRegistry, this._patternRegistry, this._pixelRatio)
|
|
@@ -263,7 +224,7 @@ export default class MaplibreLayerAdapter extends LayerAdapter {
|
|
|
263
224
|
if (!style?.layers) {
|
|
264
225
|
return null
|
|
265
226
|
}
|
|
266
|
-
const layer = style.layers.find(
|
|
227
|
+
const layer = style.layers.find(styleLayer => this._symbolLayerIds.has(styleLayer.id))
|
|
267
228
|
return layer?.id ?? null
|
|
268
229
|
}
|
|
269
230
|
|
|
@@ -288,38 +249,12 @@ export default class MaplibreLayerAdapter extends LayerAdapter {
|
|
|
288
249
|
})
|
|
289
250
|
}
|
|
290
251
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
*/
|
|
295
|
-
applyDatasetVisibility (datasetId) {
|
|
296
|
-
const registryDataset = datasetRegistry.getDataset(datasetId)
|
|
297
|
-
if (registryDataset) {
|
|
298
|
-
this._applyRegistryDatasetVisibility(registryDataset)
|
|
252
|
+
_setLayerVisibility (layerId, visibility) {
|
|
253
|
+
if (this._map.getLayer(layerId)) {
|
|
254
|
+
this._map.setLayoutProperty(layerId, 'visibility', visibility)
|
|
299
255
|
}
|
|
300
256
|
}
|
|
301
257
|
|
|
302
|
-
/**
|
|
303
|
-
* Apply visibility for all layers belonging to a registryDataset.
|
|
304
|
-
* @param {Object} registryDataset
|
|
305
|
-
*/
|
|
306
|
-
_applyRegistryDatasetVisibility (registryDataset) {
|
|
307
|
-
registryDataset.getLayersWithVisibility().forEach(({ layerIds, visibility }) => {
|
|
308
|
-
layerIds.forEach(layerId => {
|
|
309
|
-
if (this._map.getLayer(layerId)) {
|
|
310
|
-
this._map.setLayoutProperty(layerId, 'visibility', visibility)
|
|
311
|
-
}
|
|
312
|
-
})
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Apply visibility for all layers
|
|
318
|
-
*/
|
|
319
|
-
applyGlobalVisibility () {
|
|
320
|
-
datasetRegistry.forEachDataset(registryDataset => this._applyRegistryDatasetVisibility(registryDataset))
|
|
321
|
-
}
|
|
322
|
-
|
|
323
258
|
_applyFeatureFilter (registryDataset) {
|
|
324
259
|
const layersWithFilters = registryDataset.getLayersWithFilters()
|
|
325
260
|
layersWithFilters.forEach(({ layerIds, filter }) => {
|
|
@@ -331,7 +266,7 @@ export default class MaplibreLayerAdapter extends LayerAdapter {
|
|
|
331
266
|
})
|
|
332
267
|
}
|
|
333
268
|
|
|
334
|
-
|
|
269
|
+
_setLayerOpacity (layerId, opacity) {
|
|
335
270
|
const layer = this._map.getLayer(layerId)
|
|
336
271
|
if (!layer) {
|
|
337
272
|
return
|
|
@@ -214,6 +214,10 @@ describe('addDataset', () => {
|
|
|
214
214
|
await adapter.addDataset('existing-fields')
|
|
215
215
|
expect(map.moveLayer).not.toHaveBeenCalled()
|
|
216
216
|
})
|
|
217
|
+
|
|
218
|
+
it('is a no-op for an unknown dataset id (inherited from MapboxStyleLayerAdapter)', async () => {
|
|
219
|
+
await expect(adapter.addDataset('does-not-exist')).resolves.toBeUndefined()
|
|
220
|
+
})
|
|
217
221
|
})
|
|
218
222
|
|
|
219
223
|
// ─── removeDataset ────────────────────────────────────────────────────────────
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { hashString } from '../../../../../../src/utils/patternUtils.js'
|
|
1
|
+
import { MapboxStyleDataset } from '../../../registry/mapboxStyleDataset.js'
|
|
3
2
|
import { anchorToMaplibre } from '../../../../../../providers/maplibre/src/utils/symbolImages.js'
|
|
4
3
|
import { logger } from '../../../../../../src/services/logger.js'
|
|
5
4
|
const MAX_TILE_ZOOM = 22
|
|
6
5
|
|
|
7
|
-
export class MapLibreDataset extends
|
|
6
|
+
export class MapLibreDataset extends MapboxStyleDataset {
|
|
8
7
|
get fillLayerId () {
|
|
9
8
|
if (this.hasSublayers) {
|
|
10
9
|
return null
|
|
@@ -41,59 +40,10 @@ export class MapLibreDataset extends Dataset {
|
|
|
41
40
|
return null
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
get
|
|
45
|
-
if (this.hasSublayers) {
|
|
46
|
-
return this.sublayers.flatMap(sublayer => sublayer.layerIds).filter(Boolean)
|
|
47
|
-
}
|
|
43
|
+
get leafLayerIds () {
|
|
48
44
|
return [this.symbolLayerId, this.fillLayerId, this.strokeLayerId].filter(Boolean)
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
getLayersWithValue (valueName, condition = false) {
|
|
52
|
-
const response = []
|
|
53
|
-
if (condition === false || this[condition]) {
|
|
54
|
-
const layerIds = [this.symbolLayerId, this.fillLayerId, this.strokeLayerId].filter(Boolean)
|
|
55
|
-
if (layerIds.length) {
|
|
56
|
-
const value = this[valueName]
|
|
57
|
-
response.push({ layerIds, [valueName]: value })
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (this.hasSublayers) {
|
|
62
|
-
this.sublayers.forEach((sublayer) => {
|
|
63
|
-
if (condition === false || sublayer[condition]) {
|
|
64
|
-
response.push(sublayer.getLayersWithValue(valueName)[0])
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
return response
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
getLayersWithVisibility () {
|
|
72
|
-
return this.getLayersWithValue('visibility')
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
getLayersWithOpacity () {
|
|
76
|
-
return this.getLayersWithValue('opacity')
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
getLayersWithFilters () {
|
|
80
|
-
return this.getLayersWithValue('filter', 'hasHiddenFeatures')
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
get sourceId () {
|
|
84
|
-
if (this.isSublayer) { return this.parent.sourceId }
|
|
85
|
-
if (this.hasDynamicGeoJSON) { return this.dynamicGeoJSON.sourceId }
|
|
86
|
-
if (this.tiles) {
|
|
87
|
-
const tilesKey = Array.isArray(this.tiles) ? this.tiles.join(',') : this.tiles
|
|
88
|
-
return `tiles-${hashString(tilesKey)}`
|
|
89
|
-
}
|
|
90
|
-
if (this.geojson) {
|
|
91
|
-
if (typeof this.geojson === 'string') { return `geojson-${hashString(this.geojson)}` }
|
|
92
|
-
return `geojson-${this.id}`
|
|
93
|
-
}
|
|
94
|
-
return `source-${this.id}`
|
|
95
|
-
}
|
|
96
|
-
|
|
97
47
|
_geojsonIdStrategy () {
|
|
98
48
|
if (this.idProperty) { return { promoteId: this.idProperty } }
|
|
99
49
|
if (this.generateIds) { return { generateId: true } }
|
|
@@ -168,40 +118,4 @@ export class MapLibreDataset extends Dataset {
|
|
|
168
118
|
...(this.filter ? { filter: this.filter } : {})
|
|
169
119
|
}
|
|
170
120
|
}
|
|
171
|
-
|
|
172
|
-
get _hiddenFeaturesIdExpression () {
|
|
173
|
-
if (this.hasDynamicGeoJSON) {
|
|
174
|
-
return this.dynamicGeoJSON.hiddenFeaturesIdExpression
|
|
175
|
-
}
|
|
176
|
-
if (this.idProperty) {
|
|
177
|
-
return ['to-string', ['get', this.idProperty]]
|
|
178
|
-
}
|
|
179
|
-
return ['to-string', ['id']]
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
get _hiddenFeaturesFilter () {
|
|
183
|
-
const hiddenFeatures = this.hiddenFeatures?.filter(id => id !== -1)
|
|
184
|
-
if (!hiddenFeatures?.length) {
|
|
185
|
-
return null
|
|
186
|
-
}
|
|
187
|
-
return ['!', ['in', this._hiddenFeaturesIdExpression, ['literal', hiddenFeatures.map(String)]]]
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
get filter () {
|
|
191
|
-
const filter = ['all']
|
|
192
|
-
if (this.parent?.filter) {
|
|
193
|
-
filter.push(this.parent.filter)
|
|
194
|
-
}
|
|
195
|
-
if (this._datasetDefinition.filter) {
|
|
196
|
-
filter.push(this._datasetDefinition.filter)
|
|
197
|
-
}
|
|
198
|
-
const hiddenFeaturesFilter = this._hiddenFeaturesFilter
|
|
199
|
-
if (hiddenFeaturesFilter) {
|
|
200
|
-
filter.push(hiddenFeaturesFilter)
|
|
201
|
-
}
|
|
202
|
-
if (filter.length === 1) {
|
|
203
|
-
return null
|
|
204
|
-
}
|
|
205
|
-
return filter.length > 2 ? filter : filter[1]
|
|
206
|
-
}
|
|
207
121
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import Style from 'ol/style/Style.js'
|
|
2
|
+
import Fill from 'ol/style/Fill.js'
|
|
3
|
+
import Stroke from 'ol/style/Stroke.js'
|
|
4
|
+
import { getValueForStyle } from '../../../../../src/utils/getValueForStyle.js'
|
|
5
|
+
import { buildFilterEvaluator } from '../../../../../providers/beta/openlayers/src/utils/filterEvaluator.js'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_STROKE_WIDTH = 1
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Canvas-only escape hatch for genuinely crisp fill patterns, at any pixelRatio.
|
|
11
|
+
*
|
|
12
|
+
* ol/style/flat's declarative fill-pattern-src/fill-pattern-size crops a pattern to its display
|
|
13
|
+
* size rather than scaling it, so it can't hold more source detail than it shows on screen. But
|
|
14
|
+
* ol/style/Fill also accepts a raw CanvasPattern as its colour, built directly from a
|
|
15
|
+
* pixelRatio-scaled source image with no further transform — genuinely resolution-independent,
|
|
16
|
+
* since OL's pattern coordinate space is physical-pixel-equivalent (confirmed empirically).
|
|
17
|
+
*
|
|
18
|
+
* Building actual ol/style/Style objects only works on Canvas — WebGLVector requires flat-style
|
|
19
|
+
* JSON (compiled to GLSL ahead of time), so this module is Canvas-only. See layerBuilders.js's
|
|
20
|
+
* resolveLayerStyle for where this is picked over the shared flatStyle path.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const crispPatternCache = new Map() // imageId (already pixelRatio-scoped, see patternRegistry) → Fill
|
|
24
|
+
|
|
25
|
+
const cacheKey = (imageId) => imageId
|
|
26
|
+
|
|
27
|
+
export const getCachedCrispPatternFill = (imageId) => crispPatternCache.get(cacheKey(imageId))
|
|
28
|
+
|
|
29
|
+
const buildPatternFill = (imageData) => {
|
|
30
|
+
const source = document.createElement('canvas')
|
|
31
|
+
source.width = imageData.width
|
|
32
|
+
source.height = imageData.height
|
|
33
|
+
source.getContext('2d').putImageData(imageData, 0, 0)
|
|
34
|
+
const scratch = document.createElement('canvas')
|
|
35
|
+
const pattern = scratch.getContext('2d').createPattern(source, 'repeat')
|
|
36
|
+
return new Fill({ color: pattern })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Rasterise-once-cache-forever — a synchronous cache lookup at style-build time needs this
|
|
41
|
+
* already resolved.
|
|
42
|
+
* @param {Object} style - Dataset style with fillPattern* properties
|
|
43
|
+
* @param {string} mapStyleId
|
|
44
|
+
* @param {Object} patternRegistry
|
|
45
|
+
* @param {number} pixelRatio - the map's current pixelRatio; re-called on resize
|
|
46
|
+
* @returns {Promise<void>}
|
|
47
|
+
*/
|
|
48
|
+
export const registerCrispCanvasPattern = async (style, mapStyleId, patternRegistry, pixelRatio) => {
|
|
49
|
+
const imageId = patternRegistry.getPatternImageId(style, mapStyleId, pixelRatio)
|
|
50
|
+
if (!imageId || crispPatternCache.has(cacheKey(imageId))) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
const result = await patternRegistry.rasterisePatternImage(style, mapStyleId, pixelRatio)
|
|
54
|
+
if (!result) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
const key = cacheKey(result.imageId)
|
|
58
|
+
if (!crispPatternCache.has(key)) {
|
|
59
|
+
crispPatternCache.set(key, buildPatternFill(result.imageData))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Register every pattern style in the given array, in parallel. */
|
|
64
|
+
export const registerCrispCanvasPatterns = async (styleArray, mapStyleId, patternRegistry, pixelRatio) => {
|
|
65
|
+
if (!styleArray.length) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
await Promise.all(styleArray.map(style => registerCrispCanvasPattern(style, mapStyleId, patternRegistry, pixelRatio)))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Mainly for testing — clears every cached Fill. */
|
|
72
|
+
export const clearCrispPatternCache = () => {
|
|
73
|
+
crispPatternCache.clear()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const buildStroke = (registryDataset, mapStyleId) => {
|
|
77
|
+
if (!registryDataset.hasStroke) {
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
return new Stroke({
|
|
81
|
+
color: getValueForStyle(registryDataset.style.stroke, mapStyleId),
|
|
82
|
+
width: registryDataset.style.strokeWidth || DEFAULT_STROKE_WIDTH,
|
|
83
|
+
lineDash: registryDataset.style.strokeDashArray
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Builds an OL style function for a Canvas-rendered, pattern-bearing dataset/sublayer — uses a
|
|
89
|
+
* real CanvasPattern (see module doc), bypassing OpenLayersDataset.flatStyle for this one case.
|
|
90
|
+
* @param {Object} registryDataset - an OpenLayersDataset with hasPattern true
|
|
91
|
+
* @param {string} mapStyleId
|
|
92
|
+
* @param {number} pixelRatio - must match what registerCrispCanvasPattern was last called with
|
|
93
|
+
* @param {Object} patternRegistry
|
|
94
|
+
* @returns {import('ol/style/Style.js').StyleFunction}
|
|
95
|
+
*/
|
|
96
|
+
export const buildCanvasPatternStyle = (registryDataset, mapStyleId, pixelRatio, patternRegistry) => {
|
|
97
|
+
const imageId = patternRegistry.getPatternImageId(registryDataset.style, mapStyleId, pixelRatio)
|
|
98
|
+
const fill = imageId && getCachedCrispPatternFill(imageId)
|
|
99
|
+
const style = fill ? new Style({ fill, stroke: buildStroke(registryDataset, mapStyleId) }) : undefined
|
|
100
|
+
const matchesFilter = buildFilterEvaluator(registryDataset.filter)
|
|
101
|
+
|
|
102
|
+
return (feature) => {
|
|
103
|
+
if (!style) {
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
if (matchesFilter && !matchesFilter(feature)) {
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
return style
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import Style from 'ol/style/Style.js'
|
|
2
|
+
import Feature from 'ol/Feature.js'
|
|
3
|
+
import Point from 'ol/geom/Point.js'
|
|
4
|
+
import {
|
|
5
|
+
registerCrispCanvasPattern,
|
|
6
|
+
registerCrispCanvasPatterns,
|
|
7
|
+
getCachedCrispPatternFill,
|
|
8
|
+
clearCrispPatternCache,
|
|
9
|
+
buildCanvasPatternStyle
|
|
10
|
+
} from './canvasPatternStyle.js'
|
|
11
|
+
|
|
12
|
+
const OUTDOOR = 'outdoor'
|
|
13
|
+
|
|
14
|
+
const makePatternRegistry = () => ({
|
|
15
|
+
getPatternImageId: jest.fn((style, mapStyleId, pixelRatio) => `pattern-${style.fillPattern}-${mapStyleId}-${pixelRatio}`),
|
|
16
|
+
rasterisePatternImage: jest.fn(async (style, mapStyleId, pixelRatio) => ({
|
|
17
|
+
imageId: `pattern-${style.fillPattern}-${mapStyleId}-${pixelRatio}`,
|
|
18
|
+
imageData: { width: 16 * pixelRatio, height: 16 * pixelRatio }
|
|
19
|
+
}))
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
let createPatternCalls
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
clearCrispPatternCache()
|
|
25
|
+
createPatternCalls = []
|
|
26
|
+
HTMLCanvasElement.prototype.getContext = jest.fn(function () {
|
|
27
|
+
this._ctx ??= {
|
|
28
|
+
putImageData: jest.fn(),
|
|
29
|
+
createPattern: jest.fn((source, repetition) => {
|
|
30
|
+
const pattern = { source, repetition }
|
|
31
|
+
createPatternCalls.push(pattern)
|
|
32
|
+
return pattern
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
return this._ctx
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('registerCrispCanvasPattern', () => {
|
|
40
|
+
it('caches a Fill built from a real CanvasPattern for a valid pattern config', async () => {
|
|
41
|
+
const patternRegistry = makePatternRegistry()
|
|
42
|
+
const style = { fillPattern: 'dot' }
|
|
43
|
+
await registerCrispCanvasPattern(style, OUTDOOR, patternRegistry, 2)
|
|
44
|
+
const imageId = patternRegistry.getPatternImageId(style, OUTDOOR, 2)
|
|
45
|
+
const fill = getCachedCrispPatternFill(imageId)
|
|
46
|
+
expect(fill.getColor()).toEqual(expect.objectContaining({ repetition: 'repeat' }))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('rasterises at the given pixelRatio, not a fixed constant', async () => {
|
|
50
|
+
const patternRegistry = makePatternRegistry()
|
|
51
|
+
await registerCrispCanvasPattern({ fillPattern: 'dot' }, OUTDOOR, patternRegistry, 3)
|
|
52
|
+
expect(patternRegistry.rasterisePatternImage).toHaveBeenCalledWith({ fillPattern: 'dot' }, OUTDOOR, 3)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('does nothing when getPatternImageId returns null', async () => {
|
|
56
|
+
const patternRegistry = makePatternRegistry()
|
|
57
|
+
patternRegistry.getPatternImageId.mockReturnValue(null)
|
|
58
|
+
await registerCrispCanvasPattern({ fillPattern: 'unknown' }, OUTDOOR, patternRegistry, 1)
|
|
59
|
+
expect(patternRegistry.rasterisePatternImage).not.toHaveBeenCalled()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('does nothing when rasterisePatternImage returns null', async () => {
|
|
63
|
+
const patternRegistry = makePatternRegistry()
|
|
64
|
+
patternRegistry.rasterisePatternImage.mockResolvedValue(null)
|
|
65
|
+
const style = { fillPattern: 'dot' }
|
|
66
|
+
await registerCrispCanvasPattern(style, OUTDOOR, patternRegistry, 1)
|
|
67
|
+
const imageId = patternRegistry.getPatternImageId(style, OUTDOOR, 1)
|
|
68
|
+
expect(getCachedCrispPatternFill(imageId)).toBeUndefined()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('skips rasterising again once cached for the same imageId', async () => {
|
|
72
|
+
const patternRegistry = makePatternRegistry()
|
|
73
|
+
const style = { fillPattern: 'dot' }
|
|
74
|
+
await registerCrispCanvasPattern(style, OUTDOOR, patternRegistry, 1)
|
|
75
|
+
await registerCrispCanvasPattern(style, OUTDOOR, patternRegistry, 1)
|
|
76
|
+
expect(patternRegistry.rasterisePatternImage).toHaveBeenCalledTimes(1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('building the same pattern config twice concurrently still leaves exactly one cached Fill', async () => {
|
|
80
|
+
// registerCrispCanvasPatterns (plural) can be handed two different dataset styles that
|
|
81
|
+
// happen to resolve to the same imageId (identical pattern/colours) — both start rasterising
|
|
82
|
+
// before either has cached its result, so the second one back must skip re-caching rather
|
|
83
|
+
// than clobbering the first.
|
|
84
|
+
const patternRegistry = makePatternRegistry()
|
|
85
|
+
const style = { fillPattern: 'dot' }
|
|
86
|
+
await registerCrispCanvasPatterns([style, { ...style }], OUTDOOR, patternRegistry, 1)
|
|
87
|
+
const imageId = patternRegistry.getPatternImageId(style, OUTDOOR, 1)
|
|
88
|
+
expect(getCachedCrispPatternFill(imageId)).toBeDefined()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('caches separately per pixelRatio, since imageId is pixelRatio-scoped', async () => {
|
|
92
|
+
const patternRegistry = makePatternRegistry()
|
|
93
|
+
const style = { fillPattern: 'dot' }
|
|
94
|
+
await registerCrispCanvasPattern(style, OUTDOOR, patternRegistry, 1)
|
|
95
|
+
await registerCrispCanvasPattern(style, OUTDOOR, patternRegistry, 4)
|
|
96
|
+
expect(patternRegistry.rasterisePatternImage).toHaveBeenCalledTimes(2)
|
|
97
|
+
expect(getCachedCrispPatternFill(patternRegistry.getPatternImageId(style, OUTDOOR, 1))).toBeDefined()
|
|
98
|
+
expect(getCachedCrispPatternFill(patternRegistry.getPatternImageId(style, OUTDOOR, 4))).toBeDefined()
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('registerCrispCanvasPatterns', () => {
|
|
103
|
+
it('registers every config in parallel', async () => {
|
|
104
|
+
const patternRegistry = makePatternRegistry()
|
|
105
|
+
await registerCrispCanvasPatterns([{ fillPattern: 'dot' }, { fillPattern: 'stripes' }], OUTDOOR, patternRegistry, 1)
|
|
106
|
+
expect(patternRegistry.rasterisePatternImage).toHaveBeenCalledTimes(2)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('is a no-op for an empty array', async () => {
|
|
110
|
+
const patternRegistry = makePatternRegistry()
|
|
111
|
+
await registerCrispCanvasPatterns([], OUTDOOR, patternRegistry, 1)
|
|
112
|
+
expect(patternRegistry.rasterisePatternImage).not.toHaveBeenCalled()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('buildCanvasPatternStyle', () => {
|
|
117
|
+
const feature = (props) => new Feature({ geometry: new Point([0, 0]), ...props })
|
|
118
|
+
|
|
119
|
+
it('returns undefined when the pattern has not been registered yet', () => {
|
|
120
|
+
const patternRegistry = makePatternRegistry()
|
|
121
|
+
const registryDataset = { style: { fillPattern: 'dot' }, filter: null }
|
|
122
|
+
const styleFn = buildCanvasPatternStyle(registryDataset, OUTDOOR, 1, patternRegistry)
|
|
123
|
+
expect(styleFn(feature())).toBeUndefined()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('returns a Style with the cached pattern Fill once registered, for an unfiltered dataset', async () => {
|
|
127
|
+
const patternRegistry = makePatternRegistry()
|
|
128
|
+
await registerCrispCanvasPattern({ fillPattern: 'dot' }, OUTDOOR, patternRegistry, 1)
|
|
129
|
+
const registryDataset = { style: { fillPattern: 'dot' }, filter: null, hasStroke: false }
|
|
130
|
+
const styleFn = buildCanvasPatternStyle(registryDataset, OUTDOOR, 1, patternRegistry)
|
|
131
|
+
const style = styleFn(feature())
|
|
132
|
+
expect(style).toBeInstanceOf(Style)
|
|
133
|
+
expect(style.getFill().getColor()).toEqual(expect.objectContaining({ repetition: 'repeat' }))
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('includes a Stroke when the dataset has one', async () => {
|
|
137
|
+
const patternRegistry = makePatternRegistry()
|
|
138
|
+
await registerCrispCanvasPattern({ fillPattern: 'dot' }, OUTDOOR, patternRegistry, 1)
|
|
139
|
+
const registryDataset = {
|
|
140
|
+
style: { fillPattern: 'dot', stroke: '#ff0000', strokeWidth: 3 },
|
|
141
|
+
filter: null,
|
|
142
|
+
hasStroke: true
|
|
143
|
+
}
|
|
144
|
+
const styleFn = buildCanvasPatternStyle(registryDataset, OUTDOOR, 1, patternRegistry)
|
|
145
|
+
const style = styleFn(feature())
|
|
146
|
+
expect(style.getStroke().getColor()).toBe('#ff0000')
|
|
147
|
+
expect(style.getStroke().getWidth()).toBe(3)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('defaults the Stroke width to 1 when strokeWidth is not set', async () => {
|
|
151
|
+
const patternRegistry = makePatternRegistry()
|
|
152
|
+
await registerCrispCanvasPattern({ fillPattern: 'dot' }, OUTDOOR, patternRegistry, 1)
|
|
153
|
+
const registryDataset = {
|
|
154
|
+
style: { fillPattern: 'dot', stroke: '#ff0000' },
|
|
155
|
+
filter: null,
|
|
156
|
+
hasStroke: true
|
|
157
|
+
}
|
|
158
|
+
const styleFn = buildCanvasPatternStyle(registryDataset, OUTDOOR, 1, patternRegistry)
|
|
159
|
+
expect(styleFn(feature()).getStroke().getWidth()).toBe(1)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('evaluates a filter correctly for a feature with no geometry', async () => {
|
|
163
|
+
const patternRegistry = makePatternRegistry()
|
|
164
|
+
await registerCrispCanvasPattern({ fillPattern: 'dot' }, OUTDOOR, patternRegistry, 1)
|
|
165
|
+
const registryDataset = {
|
|
166
|
+
style: { fillPattern: 'dot' },
|
|
167
|
+
filter: ['==', ['get', 'category'], 'a'],
|
|
168
|
+
hasStroke: false
|
|
169
|
+
}
|
|
170
|
+
const styleFn = buildCanvasPatternStyle(registryDataset, OUTDOOR, 1, patternRegistry)
|
|
171
|
+
const featureWithNoGeometry = new Feature({ category: 'a' })
|
|
172
|
+
expect(styleFn(featureWithNoGeometry)).toBeInstanceOf(Style)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('hides a feature that does not match the dataset filter', async () => {
|
|
176
|
+
const patternRegistry = makePatternRegistry()
|
|
177
|
+
await registerCrispCanvasPattern({ fillPattern: 'dot' }, OUTDOOR, patternRegistry, 1)
|
|
178
|
+
const registryDataset = {
|
|
179
|
+
style: { fillPattern: 'dot' },
|
|
180
|
+
filter: ['==', ['get', 'category'], 'a'],
|
|
181
|
+
hasStroke: false
|
|
182
|
+
}
|
|
183
|
+
const styleFn = buildCanvasPatternStyle(registryDataset, OUTDOOR, 1, patternRegistry)
|
|
184
|
+
expect(styleFn(feature({ category: 'b' }))).toBeUndefined()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('shows a feature that matches the dataset filter', async () => {
|
|
188
|
+
const patternRegistry = makePatternRegistry()
|
|
189
|
+
await registerCrispCanvasPattern({ fillPattern: 'dot' }, OUTDOOR, patternRegistry, 1)
|
|
190
|
+
const registryDataset = {
|
|
191
|
+
style: { fillPattern: 'dot' },
|
|
192
|
+
filter: ['==', ['get', 'category'], 'a'],
|
|
193
|
+
hasStroke: false
|
|
194
|
+
}
|
|
195
|
+
const styleFn = buildCanvasPatternStyle(registryDataset, OUTDOOR, 1, patternRegistry)
|
|
196
|
+
expect(styleFn(feature({ category: 'a' }))).toBeInstanceOf(Style)
|
|
197
|
+
})
|
|
198
|
+
})
|