@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
@@ -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
- map: mapProvider.map,
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
- map: args.mapProvider.map
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 as array [west, south, east, north]
3
- * @param {Object} map - Map instance
4
- * @returns {number[]} bbox as [west, south, east, north]
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 = (map) => {
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 [west, south, east, north] from a map bounds object', () => {
7
- const map = {
8
- getBounds: () => ({
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}