@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.
- 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/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/services/patternRegistry.js +38 -0
- package/src/services/patternRegistry.test.js +61 -0
- package/src/types.js +11 -0
- package/webpack.dev.mjs +1 -0
- /package/plugins/datasets/dist/esm/{esriLayerAdapter.js → im-datasets-esri-adapter.js} +0 -0
|
@@ -221,6 +221,26 @@ describe('DatasetsInit', () => {
|
|
|
221
221
|
expect(onMapStyleChange).toHaveBeenCalled()
|
|
222
222
|
delete layerAdapter.onMapStyleChange
|
|
223
223
|
})
|
|
224
|
+
|
|
225
|
+
// MapLibre gets this re-apply for free (map.addImage()/setPaintProperty() naturally
|
|
226
|
+
// re-trigger its own MAP_DATA_CHANGE via the GL engine's styledata event) — OL doesn't (its
|
|
227
|
+
// MAP_DATA_CHANGE is tied only to the basemap tile source's tileloadend, see appEvents.js),
|
|
228
|
+
// so this emit is what lets the interact plugin's settle-window re-apply pick up newly
|
|
229
|
+
// re-resolved symbols/patterns after a style change, instead of a stale highlight sticking.
|
|
230
|
+
it('emits MAP_DATA_CHANGE once onMapStyleChange resolves, so highlights re-resolve for the new theme', async () => {
|
|
231
|
+
let resolveOnMapStyleChange
|
|
232
|
+
const onMapStyleChange = jest.fn(() => new Promise(resolve => { resolveOnMapStyleChange = resolve }))
|
|
233
|
+
Object.assign(layerAdapter, { onMapStyleChange })
|
|
234
|
+
const props = makeProps()
|
|
235
|
+
await render(<DatasetsInit {...props} />)
|
|
236
|
+
expect(props.services.eventBus.emit).not.toHaveBeenCalledWith(EVENTS.MAP_DATA_CHANGE)
|
|
237
|
+
await act(async () => {
|
|
238
|
+
resolveOnMapStyleChange()
|
|
239
|
+
await Promise.resolve()
|
|
240
|
+
})
|
|
241
|
+
expect(props.services.eventBus.emit).toHaveBeenCalledWith(EVENTS.MAP_DATA_CHANGE)
|
|
242
|
+
delete layerAdapter.onMapStyleChange
|
|
243
|
+
})
|
|
224
244
|
})
|
|
225
245
|
|
|
226
246
|
// ─── globals effect ──────────────────────────────────────────────────────────
|
|
@@ -44,7 +44,7 @@ export const initialiseDatasets = ({
|
|
|
44
44
|
const { dynamicGeoJSON } = registryDataset
|
|
45
45
|
const dynamicSource = createDynamicSource({
|
|
46
46
|
dynamicGeoJSON,
|
|
47
|
-
|
|
47
|
+
mapProvider,
|
|
48
48
|
onUpdate: (datasetId, geojson) => adapter.setData(datasetId, geojson)
|
|
49
49
|
})
|
|
50
50
|
dynamicSources.set(registryDataset.id, dynamicSource)
|
|
@@ -137,7 +137,7 @@ describe('initialiseDatasets', () => {
|
|
|
137
137
|
|
|
138
138
|
expect(createDynamicSource).toHaveBeenCalledWith(expect.objectContaining({
|
|
139
139
|
dynamicGeoJSON: dynamicDataset.dynamicGeoJSON,
|
|
140
|
-
|
|
140
|
+
mapProvider: args.mapProvider
|
|
141
141
|
}))
|
|
142
142
|
})
|
|
143
143
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Dataset } from './dataset.js'
|
|
2
|
+
import { hashString } from '../../../../src/utils/patternUtils.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Adds the Mapbox Style Spec's declarative model on top of Dataset: filter expressions,
|
|
6
|
+
* the sources/layers split (sourceId/leafLayerIds/layerIds), and aggregating opacity/
|
|
7
|
+
* visibility/filters across layers. Shared by MapLibre and OpenLayers (both speak this
|
|
8
|
+
* expression dialect); Esri has no equivalent, so EsriDataset extends Dataset directly.
|
|
9
|
+
*/
|
|
10
|
+
export class MapboxStyleDataset extends Dataset {
|
|
11
|
+
get _hiddenFeaturesIdExpression () {
|
|
12
|
+
if (this.hasDynamicGeoJSON) {
|
|
13
|
+
return this.dynamicGeoJSON.hiddenFeaturesIdExpression
|
|
14
|
+
}
|
|
15
|
+
if (this.idProperty) {
|
|
16
|
+
return ['to-string', ['get', this.idProperty]]
|
|
17
|
+
}
|
|
18
|
+
return ['to-string', ['id']]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get _hiddenFeaturesFilter () {
|
|
22
|
+
const hiddenFeatures = this.hiddenFeatures?.filter(id => id !== -1)
|
|
23
|
+
if (!hiddenFeatures?.length) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
return ['!', ['in', this._hiddenFeaturesIdExpression, ['literal', hiddenFeatures.map(String)]]]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get filter () {
|
|
30
|
+
const filter = ['all']
|
|
31
|
+
if (this.parent?.filter) {
|
|
32
|
+
filter.push(this.parent.filter)
|
|
33
|
+
}
|
|
34
|
+
if (this._datasetDefinition.filter) {
|
|
35
|
+
filter.push(this._datasetDefinition.filter)
|
|
36
|
+
}
|
|
37
|
+
const hiddenFeaturesFilter = this._hiddenFeaturesFilter
|
|
38
|
+
if (hiddenFeaturesFilter) {
|
|
39
|
+
filter.push(hiddenFeaturesFilter)
|
|
40
|
+
}
|
|
41
|
+
if (filter.length === 1) {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
return filter.length > 2 ? filter : filter[1]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Layer ids this dataset renders to — override per adapter (MapLibre: up to 3; OL: at most 1).
|
|
48
|
+
get leafLayerIds () {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get layerIds () {
|
|
53
|
+
if (this.hasSublayers) {
|
|
54
|
+
return this.sublayers.flatMap(sublayer => sublayer.layerIds).filter(Boolean)
|
|
55
|
+
}
|
|
56
|
+
return this.leafLayerIds
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getLayersWithValue (valueName, condition = false) {
|
|
60
|
+
const response = []
|
|
61
|
+
if (condition === false || this[condition]) {
|
|
62
|
+
const layerIds = this.leafLayerIds
|
|
63
|
+
if (layerIds.length) {
|
|
64
|
+
const value = this[valueName]
|
|
65
|
+
response.push({ layerIds, [valueName]: value })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (this.hasSublayers) {
|
|
70
|
+
this.sublayers.forEach((sublayer) => {
|
|
71
|
+
if (condition === false || sublayer[condition]) {
|
|
72
|
+
// A sublayer can legitimately contribute nothing (empty leafLayerIds).
|
|
73
|
+
const [entry] = sublayer.getLayersWithValue(valueName)
|
|
74
|
+
if (entry) {
|
|
75
|
+
response.push(entry)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
return response
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getLayersWithVisibility () {
|
|
84
|
+
return this.getLayersWithValue('visibility')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getLayersWithOpacity () {
|
|
88
|
+
return this.getLayersWithValue('opacity')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getLayersWithFilters () {
|
|
92
|
+
return this.getLayersWithValue('filter', 'hasHiddenFeatures')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get sourceId () {
|
|
96
|
+
if (this.isSublayer) { return this.parent.sourceId }
|
|
97
|
+
if (this.hasDynamicGeoJSON) { return this.dynamicGeoJSON.sourceId }
|
|
98
|
+
if (this.tiles) {
|
|
99
|
+
const tilesKey = Array.isArray(this.tiles) ? this.tiles.join(',') : this.tiles
|
|
100
|
+
return `tiles-${hashString(tilesKey)}`
|
|
101
|
+
}
|
|
102
|
+
if (this.geojson) {
|
|
103
|
+
if (typeof this.geojson === 'string') { return `geojson-${hashString(this.geojson)}` }
|
|
104
|
+
return `geojson-${this.id}`
|
|
105
|
+
}
|
|
106
|
+
return `source-${this.id}`
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Dataset } from './dataset.js'
|
|
2
|
+
import { MapboxStyleDataset } from './mapboxStyleDataset.js'
|
|
3
|
+
import { datasetRegistry } from './datasetRegistry.js'
|
|
4
|
+
import { attachGlobalState } from './globalDataset.js'
|
|
5
|
+
import { hashString } from '../../../../src/utils/patternUtils.js'
|
|
6
|
+
// Use the mock datasetRegistry with the demo datasets attached before each test
|
|
7
|
+
jest.mock('./datasetRegistry.js')
|
|
8
|
+
|
|
9
|
+
const globalState = {
|
|
10
|
+
opacityMode: 'dataset',
|
|
11
|
+
opacity: 1,
|
|
12
|
+
visible: true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('MapboxStyleDataset', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
datasetRegistry.attachCreateDataset(def => new MapboxStyleDataset(def))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('_hiddenFeaturesIdExpression', () => {
|
|
21
|
+
it('uses get(idProperty) when idProperty is set', () => {
|
|
22
|
+
const dataset = new MapboxStyleDataset({ idProperty: 'ref' })
|
|
23
|
+
expect(dataset._hiddenFeaturesIdExpression).toEqual(['to-string', ['get', 'ref']])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('uses the feature id when idProperty is not set', () => {
|
|
27
|
+
const dataset = new MapboxStyleDataset({})
|
|
28
|
+
expect(dataset._hiddenFeaturesIdExpression).toEqual(['to-string', ['id']])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('uses the dynamicGeoJSON hiddenFeaturesIdExpression when the dataset is dynamic', () => {
|
|
32
|
+
const dataset = new MapboxStyleDataset({ dynamicGeoJSON: { idProperty: 'ref' } })
|
|
33
|
+
expect(dataset._hiddenFeaturesIdExpression).toEqual(['to-string', ['get', 'ref']])
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('filter', () => {
|
|
38
|
+
it('returns null when there is no filter and no hidden features', () => {
|
|
39
|
+
const dataset = new MapboxStyleDataset({})
|
|
40
|
+
expect(dataset.filter).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns its own filter expression directly when it is the only filter', () => {
|
|
44
|
+
const dataset = new MapboxStyleDataset({ filter: ['==', ['get', 'type'], 'foo'] })
|
|
45
|
+
expect(dataset.filter).toEqual(['==', ['get', 'type'], 'foo'])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns the hidden features filter directly when it is the only filter', () => {
|
|
49
|
+
const dataset = new MapboxStyleDataset({ hiddenFeatures: [1, 2] })
|
|
50
|
+
expect(dataset.filter).toEqual(['!', ['in', ['to-string', ['id']], ['literal', ['1', '2']]]])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('combines its own filter and the hidden features filter with "all"', () => {
|
|
54
|
+
const dataset = new MapboxStyleDataset({ filter: ['==', ['get', 'type'], 'foo'], hiddenFeatures: [5] })
|
|
55
|
+
expect(dataset.filter).toEqual([
|
|
56
|
+
'all',
|
|
57
|
+
['==', ['get', 'type'], 'foo'],
|
|
58
|
+
['!', ['in', ['to-string', ['id']], ['literal', ['5']]]]
|
|
59
|
+
])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('includes the parent filter for a sublayer', () => {
|
|
63
|
+
const parentDef = { id: 'parent', filter: ['==', ['get', 'cat'], 'a'] }
|
|
64
|
+
const childDef = { id: 'child', parentId: 'parent', filter: ['==', ['get', 'sub'], 'b'] }
|
|
65
|
+
datasetRegistry.attach({ parent: parentDef, child: childDef })
|
|
66
|
+
expect(new MapboxStyleDataset(childDef).filter).toEqual(['all', parentDef.filter, childDef.filter])
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('sourceId', () => {
|
|
71
|
+
it('returns a tiles-based id for an array of tiles URLs', () => {
|
|
72
|
+
const dataset = new MapboxStyleDataset({ id: 'ds', tiles: ['https://example.com/{z}/{x}/{y}'] })
|
|
73
|
+
expect(dataset.sourceId).toBe(`tiles-${hashString('https://example.com/{z}/{x}/{y}')}`)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns geojson-{hash} for a static string geojson url', () => {
|
|
77
|
+
const dataset = new MapboxStyleDataset({ id: 'ds', geojson: 'https://example.com/data.geojson' })
|
|
78
|
+
expect(dataset.sourceId).toBe(`geojson-${hashString('https://example.com/data.geojson')}`)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('returns geojson-{id} for an inline geojson object', () => {
|
|
82
|
+
const dataset = new MapboxStyleDataset({ id: 'ds', geojson: { type: 'FeatureCollection', features: [] } })
|
|
83
|
+
expect(dataset.sourceId).toBe('geojson-ds')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('returns source-{id} when there are no tiles and no geojson', () => {
|
|
87
|
+
const dataset = new MapboxStyleDataset({ id: 'ds' })
|
|
88
|
+
expect(dataset.sourceId).toBe('source-ds')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('returns the parent sourceId for a sublayer', () => {
|
|
92
|
+
const parentDef = { id: 'parent', geojson: { type: 'FeatureCollection', features: [] } }
|
|
93
|
+
const childDef = { id: 'child', parentId: 'parent' }
|
|
94
|
+
datasetRegistry.attach({ parent: parentDef, child: childDef })
|
|
95
|
+
expect(new MapboxStyleDataset(childDef).sourceId).toBe('geojson-parent')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('leafLayerIds / layerIds', () => {
|
|
100
|
+
it('leafLayerIds defaults to an empty array on the base class', () => {
|
|
101
|
+
expect(new MapboxStyleDataset({ id: 'ds' }).leafLayerIds).toEqual([])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('layerIds returns leafLayerIds for a dataset with no sublayers', () => {
|
|
105
|
+
class FakeAdapterDataset extends MapboxStyleDataset {
|
|
106
|
+
get leafLayerIds () { return [this.id] }
|
|
107
|
+
}
|
|
108
|
+
const dataset = new FakeAdapterDataset({ id: 'ds' })
|
|
109
|
+
expect(dataset.layerIds).toEqual(['ds'])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('layerIds flattens leafLayerIds from all sublayers', () => {
|
|
113
|
+
class FakeAdapterDataset extends MapboxStyleDataset {
|
|
114
|
+
get leafLayerIds () { return this.hasSublayers ? [] : [this.id] }
|
|
115
|
+
}
|
|
116
|
+
// attachCreateDataset mutates the shared datasetRegistry singleton, which beforeEach's
|
|
117
|
+
// attach() call alone does not reset — restore this file's own default afterward so this
|
|
118
|
+
// doesn't leak into other tests in this file.
|
|
119
|
+
try {
|
|
120
|
+
datasetRegistry.attachCreateDataset(def => new FakeAdapterDataset(def))
|
|
121
|
+
const parentDef = { id: 'parent', sublayerIds: ['child-a', 'child-b'] }
|
|
122
|
+
const childA = { id: 'child-a', parentId: 'parent' }
|
|
123
|
+
const childB = { id: 'child-b', parentId: 'parent' }
|
|
124
|
+
datasetRegistry.attach({ parent: parentDef, 'child-a': childA, 'child-b': childB })
|
|
125
|
+
expect(datasetRegistry.getDataset('parent').layerIds).toEqual(['child-a', 'child-b'])
|
|
126
|
+
} finally {
|
|
127
|
+
datasetRegistry.attachCreateDataset(def => new MapboxStyleDataset(def))
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// getLayersWithValue's recursion/condition-gating/aggregation logic is shared by all three
|
|
133
|
+
// methods below and adapter-agnostic — leafLayerIds itself is the one adapter-specific
|
|
134
|
+
// override point (see leafLayerIds/layerIds tests above), so a minimal one-id-per-leaf
|
|
135
|
+
// FakeAdapterDataset is enough to exercise it directly, without depending on a real adapter's
|
|
136
|
+
// test suite (e.g. MapLibreDataset's) to incidentally cover this shared behaviour.
|
|
137
|
+
describe('getLayersWithVisibility / getLayersWithOpacity / getLayersWithFilters', () => {
|
|
138
|
+
class FakeAdapterDataset extends MapboxStyleDataset {
|
|
139
|
+
get leafLayerIds () { return this.hasSublayers ? [] : [this.id] }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
// visibility/opacity both fold in global state (see globalDataset.js) — set explicitly
|
|
144
|
+
// here rather than relying on whatever an earlier test in this file last left it as.
|
|
145
|
+
attachGlobalState(globalState)
|
|
146
|
+
datasetRegistry.attachCreateDataset(def => new FakeAdapterDataset(def))
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
afterEach(() => {
|
|
150
|
+
datasetRegistry.attachCreateDataset(def => new MapboxStyleDataset(def))
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('getLayersWithVisibility', () => {
|
|
154
|
+
it('returns an entry with layerIds and visibility for each sublayer', () => {
|
|
155
|
+
// A sublayer's own visible getter requires the parent to also be visible (see
|
|
156
|
+
// dataset.js's visible getter) — visible: true here is the parent's own, needed for
|
|
157
|
+
// childA to end up genuinely visible below.
|
|
158
|
+
const parentDef = { id: 'parent', visible: true, sublayerIds: ['child-a', 'child-b'] }
|
|
159
|
+
const childA = { id: 'child-a', parentId: 'parent', visible: true }
|
|
160
|
+
const childB = { id: 'child-b', parentId: 'parent', visible: false }
|
|
161
|
+
datasetRegistry.attach({ parent: parentDef, 'child-a': childA, 'child-b': childB })
|
|
162
|
+
expect(datasetRegistry.getDataset('parent').getLayersWithVisibility()).toEqual([
|
|
163
|
+
{ layerIds: ['child-a'], visibility: 'visible' },
|
|
164
|
+
{ layerIds: ['child-b'], visibility: 'none' }
|
|
165
|
+
])
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('getLayersWithOpacity', () => {
|
|
170
|
+
it('returns layerIds and opacity for a top-level dataset with no sublayers', () => {
|
|
171
|
+
const dataset = new FakeAdapterDataset({ id: 'ds', style: { opacity: 0.4 } })
|
|
172
|
+
expect(dataset.getLayersWithOpacity()).toEqual([{ layerIds: ['ds'], opacity: 0.4 }])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('returns layerIds and opacity for each sublayer', () => {
|
|
176
|
+
const parentDef = { id: 'parent', sublayerIds: ['child-a', 'child-b'] }
|
|
177
|
+
const childA = { id: 'child-a', parentId: 'parent', style: { opacity: 0.75 } }
|
|
178
|
+
const childB = { id: 'child-b', parentId: 'parent' }
|
|
179
|
+
datasetRegistry.attach({ parent: parentDef, 'child-a': childA, 'child-b': childB })
|
|
180
|
+
expect(datasetRegistry.getDataset('parent').getLayersWithOpacity()).toEqual([
|
|
181
|
+
{ layerIds: ['child-a'], opacity: 0.75 },
|
|
182
|
+
{ layerIds: ['child-b'], opacity: 1 }
|
|
183
|
+
])
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('skips a sublayer that contributes no leaf layers, rather than pushing undefined', () => {
|
|
187
|
+
// Models an adapter (like OpenLayersDataset today) where some styles aren't renderable
|
|
188
|
+
// yet — such a sublayer's leafLayerIds is empty, and that must not surface as an
|
|
189
|
+
// undefined entry in the result.
|
|
190
|
+
class UnevenAdapterDataset extends MapboxStyleDataset {
|
|
191
|
+
get leafLayerIds () { return this.style?.renderable ? [this.id] : [] }
|
|
192
|
+
}
|
|
193
|
+
datasetRegistry.attachCreateDataset(def => new UnevenAdapterDataset(def))
|
|
194
|
+
const parentDef = { id: 'parent', sublayerIds: ['child-renderable', 'child-unsupported'] }
|
|
195
|
+
const childRenderable = { id: 'child-renderable', parentId: 'parent', style: { renderable: true, opacity: 0.5 } }
|
|
196
|
+
const childUnsupported = { id: 'child-unsupported', parentId: 'parent', style: { opacity: 0.9 } }
|
|
197
|
+
datasetRegistry.attach({ parent: parentDef, 'child-renderable': childRenderable, 'child-unsupported': childUnsupported })
|
|
198
|
+
expect(datasetRegistry.getDataset('parent').getLayersWithOpacity()).toEqual([
|
|
199
|
+
{ layerIds: ['child-renderable'], opacity: 0.5 }
|
|
200
|
+
])
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('getLayersWithFilters', () => {
|
|
205
|
+
it('returns an empty array when there are no hidden features and no sublayers', () => {
|
|
206
|
+
const dataset = new FakeAdapterDataset({ id: 'ds' })
|
|
207
|
+
expect(dataset.getLayersWithFilters()).toEqual([])
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('returns an entry with layerIds and filter when the dataset has hidden features', () => {
|
|
211
|
+
const dataset = new FakeAdapterDataset({ id: 'ds', hiddenFeatures: [1, 2] })
|
|
212
|
+
expect(dataset.getLayersWithFilters()).toEqual([{
|
|
213
|
+
layerIds: ['ds'],
|
|
214
|
+
filter: ['!', ['in', ['to-string', ['id']], ['literal', ['1', '2']]]]
|
|
215
|
+
}])
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('includes sublayer entries when a sublayer has hidden features', () => {
|
|
219
|
+
const parentDef = { id: 'parent', sublayerIds: ['child'] }
|
|
220
|
+
const childDef = { id: 'child', parentId: 'parent', hiddenFeatures: [7] }
|
|
221
|
+
datasetRegistry.attach({ parent: parentDef, child: childDef })
|
|
222
|
+
expect(datasetRegistry.getDataset('parent').getLayersWithFilters()).toEqual([{
|
|
223
|
+
layerIds: ['child'],
|
|
224
|
+
filter: ['!', ['in', ['to-string', ['id']], ['literal', ['7']]]]
|
|
225
|
+
}])
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('returns an empty array when sublayers exist but none have hidden features', () => {
|
|
229
|
+
const parentDef = { id: 'parent', sublayerIds: ['child'] }
|
|
230
|
+
const childDef = { id: 'child', parentId: 'parent' }
|
|
231
|
+
datasetRegistry.attach({ parent: parentDef, child: childDef })
|
|
232
|
+
expect(datasetRegistry.getDataset('parent').getLayersWithFilters()).toEqual([])
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('extends the base Dataset class', () => {
|
|
238
|
+
expect(new MapboxStyleDataset({})).toBeInstanceOf(Dataset)
|
|
239
|
+
})
|
|
240
|
+
})
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Get bbox from map
|
|
3
|
-
*
|
|
4
|
-
* @
|
|
2
|
+
* Get the current viewport bbox from a map provider — both providers' getBounds() already
|
|
3
|
+
* return this exact [minX, minY, maxX, maxY] shape in their own projection.
|
|
4
|
+
* @param {Object} mapProvider - Map provider instance (not the raw map)
|
|
5
|
+
* @returns {number[]} bbox as [west, south, east, north] (or [minX, minY, maxX, maxY])
|
|
5
6
|
*/
|
|
6
|
-
export const getBboxArray = (
|
|
7
|
-
const bounds = map.getBounds()
|
|
8
|
-
return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]
|
|
9
|
-
}
|
|
7
|
+
export const getBboxArray = (mapProvider) => mapProvider.getBounds()
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
10
|
* Check if inner bbox is fully contained within outer bbox
|
|
@@ -3,16 +3,9 @@ import { getBboxArray, bboxContains, expandBbox, bboxIntersects, getGeometryBbox
|
|
|
3
3
|
// ─── getBboxArray ─────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
describe('getBboxArray', () => {
|
|
6
|
-
it('returns
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
getWest: () => -1,
|
|
10
|
-
getSouth: () => 50,
|
|
11
|
-
getEast: () => 1,
|
|
12
|
-
getNorth: () => 52
|
|
13
|
-
})
|
|
14
|
-
}
|
|
15
|
-
expect(getBboxArray(map)).toEqual([-1, 50, 1, 52])
|
|
6
|
+
it('returns the mapProvider.getBounds() array as-is', () => {
|
|
7
|
+
const mapProvider = { getBounds: () => [-1, 50, 1, 52] }
|
|
8
|
+
expect(getBboxArray(mapProvider)).toEqual([-1, 50, 1, 52])
|
|
16
9
|
})
|
|
17
10
|
})
|
|
18
11
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.touch-vertex-target circle{fill:var(--map-overlay-foreground-color)}.touch-vertex-target path{fill:var(--map-overlay-halo-color)}.im-draw-touch-target{color:#3b82f6;cursor:grab;filter:drop-shadow(0 2px 4px rgba(0,0,0,.15))}.im-draw-touch-target:active{cursor:grabbing}.im-draw-button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}.im-c-actions .im-c-button-wrapper--draw-cancel,.im-c-actions .im-c-button-wrapper--draw-done,.im-c-actions .im-c-button-wrapper--draw-menu{width:33.33%}.im-o-app--desktop .im-c-actions .im-c-button-wrapper--draw-cancel,.im-o-app--desktop .im-c-actions .im-c-button-wrapper--draw-done,.im-o-app--desktop .im-c-actions .im-c-button-wrapper--draw-menu,.im-o-app--tablet .im-c-actions .im-c-button-wrapper--draw-cancel,.im-o-app--tablet .im-c-actions .im-c-button-wrapper--draw-done,.im-o-app--tablet .im-c-actions .im-c-button-wrapper--draw-menu{width:100px}
|