@defra/interactive-map 0.0.30-alpha → 0.0.31-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/context.md +4 -4
  7. package/docs/api.md +111 -37
  8. package/docs/assets/css/docusaurus.scss +20 -4
  9. package/docs/assets/images/basic-map.jpg +0 -0
  10. package/docs/assets/images/button-first.jpg +0 -0
  11. package/docs/assets/images/deuteranopia-ozs-map-thumb.jpg +0 -0
  12. package/docs/assets/images/draw.jpg +0 -0
  13. package/docs/assets/images/marker-panel.jpg +0 -0
  14. package/docs/assets/images/night-ozs-map-thumb.jpg +0 -0
  15. package/docs/assets/images/outdoor-ozs-map-thumb.jpg +0 -0
  16. package/docs/assets/images/search.jpg +0 -0
  17. package/docs/assets/images/select-feature.jpg +0 -0
  18. package/docs/assets/images/style-switcher.jpg +0 -0
  19. package/docs/assets/images/toggle-marker-label.jpg +0 -0
  20. package/docs/assets/images/tritanopia-ozs-map-thumb.jpg +0 -0
  21. package/docs/examples/add-marker-with-panel.mdx +2 -2
  22. package/docs/examples/basic-map.mdx +2 -2
  23. package/docs/examples/draw.mdx +271 -0
  24. package/docs/examples/index.mdx +58 -10
  25. package/docs/examples/search-control.mdx +71 -0
  26. package/docs/examples/select-feature.mdx +146 -0
  27. package/docs/examples/style-switcher.mdx +135 -0
  28. package/docs/examples/toggle-marker-label.mdx +3 -3
  29. package/docs/plugins/datasets.md +15 -28
  30. package/docusaurus.config.cjs +5 -1
  31. package/package.json +1 -1
  32. package/plugins/beta/datasets/dist/css/index.css +4 -0
  33. package/plugins/beta/datasets/dist/esm/im-datasets-ml-adapter.js +1 -0
  34. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  35. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  36. package/plugins/beta/datasets/dist/umd/im-datasets-ml-adapter.js +2 -0
  37. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -0
  38. package/plugins/beta/datasets/dist/umd/index.js +2 -0
  39. package/plugins/beta/datasets/src/adapters/loadLayerAdapter.js +33 -0
  40. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +44 -120
  41. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +145 -245
  42. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +516 -0
  43. package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +191 -0
  44. package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +441 -0
  45. package/plugins/beta/datasets/src/api/addDataset.js +2 -5
  46. package/plugins/beta/datasets/src/api/addDataset.test.js +16 -0
  47. package/plugins/beta/datasets/src/api/getOpacity.js +14 -14
  48. package/plugins/beta/datasets/src/api/getOpacity.test.js +72 -0
  49. package/plugins/beta/datasets/src/api/getStyle.js +8 -9
  50. package/plugins/beta/datasets/src/api/getStyle.test.js +42 -0
  51. package/plugins/beta/datasets/src/api/removeDataset.js +7 -7
  52. package/plugins/beta/datasets/src/api/removeDataset.test.js +45 -0
  53. package/plugins/beta/datasets/src/api/setData.js +9 -3
  54. package/plugins/beta/datasets/src/api/setData.test.js +62 -0
  55. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +5 -30
  56. package/plugins/beta/datasets/src/api/setDatasetVisibility.test.js +31 -0
  57. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +3 -21
  58. package/plugins/beta/datasets/src/api/setFeatureVisibility.test.js +32 -0
  59. package/plugins/beta/datasets/src/api/setGlobals.js +20 -0
  60. package/plugins/beta/datasets/src/api/setGlobals.test.js +63 -0
  61. package/plugins/beta/datasets/src/api/setOpacity.js +8 -27
  62. package/plugins/beta/datasets/src/api/setOpacity.test.js +31 -0
  63. package/plugins/beta/datasets/src/api/setStyle.js +5 -21
  64. package/plugins/beta/datasets/src/api/setStyle.test.js +53 -0
  65. package/plugins/beta/datasets/src/components/Key/Key.jsx +51 -0
  66. package/plugins/beta/datasets/src/components/Key/KeyGroupItem.jsx +18 -0
  67. package/plugins/beta/datasets/src/components/Key/KeyItem.jsx +20 -0
  68. package/plugins/beta/datasets/src/components/Key/KeySvg.jsx +22 -0
  69. package/plugins/beta/datasets/src/components/Key/KeySvg.test.jsx +87 -0
  70. package/plugins/beta/datasets/src/components/Key/KeySvgLine.jsx +19 -0
  71. package/plugins/beta/datasets/src/components/Key/KeySvgLine.test.jsx +71 -0
  72. package/plugins/beta/datasets/src/components/{KeySvgPattern.jsx → Key/KeySvgPattern.jsx} +3 -4
  73. package/plugins/beta/datasets/src/components/Key/KeySvgPattern.test.jsx +55 -0
  74. package/plugins/beta/datasets/src/components/Key/KeySvgRect.jsx +22 -0
  75. package/plugins/beta/datasets/src/components/Key/KeySvgRect.test.jsx +85 -0
  76. package/plugins/beta/datasets/src/components/{KeySvgSymbol.jsx → Key/KeySvgSymbol.jsx} +5 -5
  77. package/plugins/beta/datasets/src/components/Key/KeySvgSymbol.test.jsx +80 -0
  78. package/plugins/beta/datasets/src/components/Key/svgProperties.test.js +61 -0
  79. package/plugins/beta/datasets/src/{panels → components/LayersMenu}/Layers.module.scss +4 -0
  80. package/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx +33 -0
  81. package/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx +31 -0
  82. package/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx +19 -0
  83. package/plugins/beta/datasets/src/datasets.scss +2 -2
  84. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +8 -8
  85. package/plugins/beta/datasets/src/{DatasetsInit.jsx → initialise/DatasetsInit.jsx} +19 -21
  86. package/plugins/beta/datasets/src/{defaults.js → initialise/defaults.js} +17 -2
  87. package/plugins/beta/datasets/src/initialise/defaults.test.js +127 -0
  88. package/plugins/beta/datasets/src/{datasets.js → initialise/initialiseDatasets.js} +30 -26
  89. package/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js +273 -0
  90. package/plugins/beta/datasets/src/manifest.js +8 -6
  91. package/plugins/beta/datasets/src/reducers/__data__/demoDatasets.js +253 -0
  92. package/plugins/beta/datasets/src/reducers/datasetsToMenu.js +78 -0
  93. package/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js +35 -0
  94. package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.js +34 -0
  95. package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.test.js +117 -0
  96. package/plugins/beta/datasets/src/reducers/pluginState.js +206 -0
  97. package/plugins/beta/datasets/src/reducers/pluginState.test.js +386 -0
  98. package/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js +29 -0
  99. package/plugins/beta/datasets/src/registry/createDataset.js +5 -0
  100. package/plugins/beta/datasets/src/registry/dataset.js +172 -0
  101. package/plugins/beta/datasets/src/registry/dataset.test.js +483 -0
  102. package/plugins/beta/datasets/src/registry/datasetDefinitionCache.js +35 -0
  103. package/plugins/beta/datasets/src/registry/datasetRegistry.js +116 -0
  104. package/plugins/beta/datasets/src/registry/datasetRegistry.test.js +335 -0
  105. package/plugins/beta/datasets/src/registry/dynamicGeoJson.js +17 -0
  106. package/plugins/beta/datasets/src/registry/globalDataset.js +36 -0
  107. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  108. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  109. package/plugins/beta/draw-ml/src/events.js +13 -1
  110. package/plugins/beta/draw-ml/src/index.js +1 -1
  111. package/plugins/beta/draw-ml/src/manifest.js +3 -0
  112. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  113. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  114. package/plugins/interact/src/InteractInit.jsx +2 -2
  115. package/plugins/interact/src/InteractInit.test.js +1 -1
  116. package/plugins/interact/src/events.js +2 -29
  117. package/plugins/interact/src/events.test.js +4 -102
  118. package/plugins/interact/src/hooks/useAttachEvents.js +3 -4
  119. package/plugins/interact/src/hooks/useAttachEvents.test.js +1 -3
  120. package/plugins/interact/src/hooks/useInteractionHandlers.js +10 -1
  121. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +23 -4
  122. package/plugins/interact/src/manifest.js +0 -35
  123. package/plugins/interact/src/manifest.test.js +3 -43
  124. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  125. package/providers/beta/esri/src/esriProvider.js +4 -0
  126. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  127. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  128. package/providers/beta/openlayers/src/openlayersProvider.js +4 -0
  129. package/providers/beta/openlayers/src/openlayersProvider.test.js +5 -0
  130. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  131. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  132. package/providers/maplibre/src/maplibreProvider.js +6 -4
  133. package/providers/maplibre/src/maplibreProvider.test.js +5 -0
  134. package/providers/maplibre/src/utils/highlightFeatures.js +74 -25
  135. package/providers/maplibre/src/utils/highlightFeatures.test.js +132 -0
  136. package/providers/maplibre/src/utils/patternImages.js +2 -4
  137. package/providers/maplibre/src/utils/symbolImages.js +1 -4
  138. package/rollup.esm.mjs +2 -8
  139. package/src/App/components/MapButton/MapButton.jsx +16 -1
  140. package/src/App/components/MapButton/MapButton.module.scss +12 -1
  141. package/src/App/components/MapButton/MapButton.test.jsx +40 -1
  142. package/src/App/hooks/useContinueEnabledEvaluator.js +31 -0
  143. package/src/App/hooks/useContinueEnabledEvaluator.test.js +118 -0
  144. package/src/App/hooks/useEvaluateProp.js +15 -15
  145. package/src/App/hooks/useEvaluateProp.test.js +80 -76
  146. package/src/App/hooks/useLayoutMeasurements.js +1 -3
  147. package/src/App/hooks/useResizeObserver.js +12 -1
  148. package/src/App/hooks/useResizeObserver.test.js +1 -0
  149. package/src/App/renderer/PluginInits.jsx +2 -0
  150. package/src/App/renderer/mapButtons.js +21 -1
  151. package/src/App/renderer/mapButtons.test.js +46 -1
  152. package/src/App/store/appReducer.js +1 -1
  153. package/src/App/store/appReducer.test.js +37 -0
  154. package/src/InteractiveMap/InteractiveMap.js +10 -0
  155. package/src/InteractiveMap/InteractiveMap.test.js +8 -0
  156. package/src/config/appConfig.js +25 -0
  157. package/src/config/appConfig.test.js +50 -0
  158. package/src/config/defaults.js +1 -0
  159. package/src/scss/settings/_dimensions.scss +1 -0
  160. package/src/services/patternRegistry.js +8 -4
  161. package/src/types.js +7 -0
  162. package/src/utils/patternUtils.js +1 -2
  163. package/src/utils/patternUtils.test.js +9 -0
  164. package/webpack.dev.mjs +10 -0
  165. package/webpack.umd.mjs +1 -1
  166. package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +0 -1
  167. package/plugins/beta/datasets/dist/adapters/maplibre/esm/index.js +0 -1
  168. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +0 -2
  169. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +0 -2
  170. package/plugins/beta/datasets/dist/umd/maplibre/index.js +0 -2
  171. package/plugins/beta/datasets/src/adapters/maplibre/index.js +0 -18
  172. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +0 -75
  173. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +0 -27
  174. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +0 -31
  175. package/plugins/beta/datasets/src/components/KeySvg.jsx +0 -24
  176. package/plugins/beta/datasets/src/components/KeySvgLine.jsx +0 -19
  177. package/plugins/beta/datasets/src/components/KeySvgRect.jsx +0 -22
  178. package/plugins/beta/datasets/src/index.maplibre.umd.js +0 -23
  179. package/plugins/beta/datasets/src/panels/Key.jsx +0 -62
  180. package/plugins/beta/datasets/src/panels/Layers.jsx +0 -141
  181. package/plugins/beta/datasets/src/reducer.js +0 -219
  182. package/plugins/beta/datasets/src/reducers/keyReducer.js +0 -34
  183. package/plugins/beta/datasets/src/utils/filters.js +0 -34
  184. package/plugins/beta/datasets/src/utils/mergeSublayer.js +0 -86
  185. package/plugins/beta/draw-ml/dist/css/index.css +0 -21
  186. /package/plugins/beta/datasets/dist/umd/{maplibre/im-datasets-maplibre-adapter.js.LICENSE.txt → im-datasets-ml-adapter.js.LICENSE.txt} +0 -0
  187. /package/plugins/beta/datasets/dist/umd/{maplibre/im-datasets-plugin.js.LICENSE.txt → im-datasets-plugin.js.LICENSE.txt} +0 -0
  188. /package/plugins/beta/datasets/dist/umd/{maplibre/index.js.LICENSE.txt → index.js.LICENSE.txt} +0 -0
  189. /package/plugins/beta/datasets/src/components/{EmptyKey.jsx → Key/EmptyKey.jsx} +0 -0
  190. /package/plugins/beta/datasets/src/components/{EmptyKey.test.jsx → Key/EmptyKey.test.jsx} +0 -0
  191. /package/plugins/beta/datasets/src/{panels → components/Key}/Key.module.scss +0 -0
  192. /package/plugins/beta/datasets/src/components/{svgProperties.js → Key/svgProperties.js} +0 -0
@@ -0,0 +1,191 @@
1
+ import { Dataset } from '../../../registry/dataset.js'
2
+ import { hashString } from '../../../../../../../src/utils/patternUtils.js'
3
+ import { anchorToMaplibre } from '../../../../../../../providers/maplibre/src/utils/symbolImages.js'
4
+ const MAX_TILE_ZOOM = 22
5
+
6
+ export class MapLibreDataset extends Dataset {
7
+ get visibility () { return this.visible ? 'visible' : 'none' }
8
+
9
+ get fillLayerId () {
10
+ if (this.hasSublayers) {
11
+ return null
12
+ }
13
+ if (this.hasSymbol) {
14
+ return null
15
+ }
16
+ if (this.hasFill) {
17
+ return this.id
18
+ }
19
+ return null
20
+ }
21
+
22
+ get strokeLayerId () {
23
+ if (this.hasSublayers) {
24
+ return null
25
+ }
26
+ if (this.hasSymbol) {
27
+ return null
28
+ }
29
+ if (this.hasStroke) {
30
+ return this.hasFill ? `${this.id}-stroke` : this.id
31
+ }
32
+ return null
33
+ }
34
+
35
+ get symbolLayerId () {
36
+ if (this.hasSublayers) {
37
+ return null
38
+ }
39
+ if (this.hasSymbol) {
40
+ return this.id
41
+ }
42
+ return null
43
+ }
44
+
45
+ get layerIds () {
46
+ if (this.hasSublayers) {
47
+ return this.sublayers.flatMap(sublayer => sublayer.layerIds).filter(Boolean)
48
+ }
49
+ return [this.symbolLayerId, this.fillLayerId, this.strokeLayerId].filter(Boolean)
50
+ }
51
+
52
+ getLayersWithValue (valueName, condition = false) {
53
+ const response = []
54
+ if (condition === false || this[condition]) {
55
+ const layerIds = [this.symbolLayerId, this.fillLayerId, this.strokeLayerId].filter(Boolean)
56
+ if (layerIds.length) {
57
+ const value = this[valueName]
58
+ response.push({ layerIds, [valueName]: value })
59
+ }
60
+ }
61
+
62
+ if (this.hasSublayers) {
63
+ this.sublayers.forEach((sublayer) => {
64
+ if (condition === false || sublayer[condition]) {
65
+ response.push(sublayer.getLayersWithValue(valueName)[0])
66
+ }
67
+ })
68
+ }
69
+ return response
70
+ }
71
+
72
+ getLayersWithVisibility () {
73
+ return this.getLayersWithValue('visibility')
74
+ }
75
+
76
+ getLayersWithOpacity () {
77
+ return this.getLayersWithValue('opacity')
78
+ }
79
+
80
+ getLayersWithFilters () {
81
+ return this.getLayersWithValue('filter', 'hasHiddenFeatures')
82
+ }
83
+
84
+ get sourceId () {
85
+ if (this.isSublayer) { return this.parent.sourceId }
86
+ if (this.hasDynamicGeoJSON) { return this.dynamicGeoJSON.sourceId }
87
+ if (this.tiles) {
88
+ const tilesKey = Array.isArray(this.tiles) ? this.tiles.join(',') : this.tiles
89
+ return `tiles-${hashString(tilesKey)}`
90
+ }
91
+ if (this.geojson) {
92
+ if (typeof this.geojson === 'string') { return `geojson-${hashString(this.geojson)}` }
93
+ return `geojson-${this.id}`
94
+ }
95
+ return `source-${this.id}`
96
+ }
97
+
98
+ get source () {
99
+ if (this.hasDynamicGeoJSON) {
100
+ return this.dynamicGeoJSON.source
101
+ }
102
+ if (this.tiles) {
103
+ return {
104
+ type: 'vector',
105
+ tiles: this.tiles,
106
+ minzoom: this.minZoom || 0,
107
+ maxzoom: this.maxZoom || MAX_TILE_ZOOM
108
+ }
109
+ }
110
+ if (this.geojson) {
111
+ return { type: 'geojson', data: this.geojson, generateId: true }
112
+ }
113
+ return null
114
+ }
115
+
116
+ getSymbolSource (imageId, anchor, symbolDef) {
117
+ return {
118
+ id: this.symbolLayerId,
119
+ type: 'symbol',
120
+ source: this.sourceId,
121
+ 'source-layer': this.sourceLayer,
122
+ minzoom: this.minZoom,
123
+ maxzoom: this.maxZoom,
124
+ layout: {
125
+ visibility: this.visibility,
126
+ 'icon-image': imageId,
127
+ 'icon-anchor': anchorToMaplibre(anchor || symbolDef?.anchor || [0.5, 0.5]),
128
+ 'icon-allow-overlap': true
129
+ },
130
+ ...(this.filter ? { filter: this.filter } : {})
131
+ }
132
+ }
133
+
134
+ getFillSource (paint) {
135
+ return {
136
+ id: this.fillLayerId,
137
+ type: 'fill',
138
+ source: this.sourceId,
139
+ 'source-layer': this.sourceLayer,
140
+ minzoom: this.minZoom,
141
+ maxzoom: this.maxZoom,
142
+ layout: { visibility: this.visibility },
143
+ paint,
144
+ ...(this.filter ? { filter: this.filter } : {})
145
+ }
146
+ }
147
+
148
+ getStrokeSource (paint) {
149
+ return {
150
+ id: this.strokeLayerId,
151
+ type: 'line',
152
+ source: this.sourceId,
153
+ 'source-layer': this.sourceLayer,
154
+ minzoom: this.minZoom,
155
+ maxzoom: this.maxZoom,
156
+ layout: { visibility: this.visibility },
157
+ paint,
158
+ ...(this.filter ? { filter: this.filter } : {})
159
+ }
160
+ }
161
+
162
+ get _hiddenFeaturesIdExpression () {
163
+ return this.hasDynamicGeoJSON ? this.dynamicGeoJSON.hiddenFeaturesIdExpression : ['to-string', ['id']]
164
+ }
165
+
166
+ get _hiddenFeaturesFilter () {
167
+ const hiddenFeatures = this.hiddenFeatures?.filter(id => id !== -1)
168
+ if (hiddenFeatures?.length) {
169
+ return ['!', ['in', this._hiddenFeaturesIdExpression, ['literal', hiddenFeatures.map(String)]]]
170
+ }
171
+ return null
172
+ }
173
+
174
+ get filter () {
175
+ const filter = ['all']
176
+ if (this.parent?.filter) {
177
+ filter.push(this.parent.filter)
178
+ }
179
+ if (this._datasetDefinition.filter) {
180
+ filter.push(this._datasetDefinition.filter)
181
+ }
182
+ const hiddenFeaturesFilter = this._hiddenFeaturesFilter
183
+ if (hiddenFeaturesFilter) {
184
+ filter.push(hiddenFeaturesFilter)
185
+ }
186
+ if (filter.length === 1) {
187
+ return null
188
+ }
189
+ return filter.length > 2 ? filter : filter[1]
190
+ }
191
+ }
@@ -0,0 +1,441 @@
1
+ import { MapLibreDataset } from './mapLibreDataset.js'
2
+ import { datasetRegistry } from '../../../registry/datasetRegistry.js'
3
+ // Use the mock datasetRegistry with the demo datasets attached before each test
4
+ // so we can test Dataset methods that depend on parent/sublayer relationships and styles
5
+ jest.mock('../../../registry/datasetRegistry.js')
6
+
7
+ describe('MapLibreDataset', () => {
8
+ beforeEach(() => {
9
+ datasetRegistry.attachCreateDataset(def => new MapLibreDataset(def))
10
+ datasetRegistry.mockExtend({
11
+ // layerIds
12
+ 'ds-fill-only': { id: 'ds-fill-only', visible: true, minZoom: 10, maxZoom: 24, style: { fill: 'blue' } },
13
+ 'ds-pattern-only': { id: 'ds-pattern-only', style: { fillPattern: 'dots' } },
14
+ 'ds-transparent-fill': { id: 'ds-transparent-fill', style: { fill: 'transparent' } },
15
+ // shared: no special properties — used by layerIds, sourceId, source, visibility,
16
+ // _hiddenFeaturesIdExpression, _hiddenFeaturesFilter, and filter tests
17
+ 'ds-bare': { id: 'ds-bare' },
18
+ 'ds-no-id-prop': { id: 'ds-no-id-prop', geojson: 'https://example.com/data', transformRequest: () => {} },
19
+ 'ds-no-transform': { id: 'ds-no-transform', geojson: 'https://example.com/data', idProperty: 'id' },
20
+ 'ds-static-url': { id: 'ds-static-url', geojson: 'https://example.com/static.geojson' },
21
+ // shared: tiles with no zoom — used by source minzoom and maxzoom fallback tests
22
+ 'ds-tiles-no-zoom': { id: 'ds-tiles-no-zoom', tiles: ['https://example.com/{z}/{x}/{y}'] },
23
+ // getSymbolSource/getFillSource/getStrokeSource 'has filter' tests use historic-monuments-prehistoric and existing-fields from demo data
24
+ // _hiddenFeaturesFilter — ds-hf-123 also reused in filter describe
25
+ 'ds-hf-123': { id: 'ds-hf-123', hiddenFeatures: [1, 2, 3] },
26
+ 'ds-hf-neg1': { id: 'ds-hf-neg1', hiddenFeatures: [-1, 5] },
27
+ 'ds-hf-all-neg1': { id: 'ds-hf-all-neg1', hiddenFeatures: [-1] },
28
+ 'ds-hf-empty': { id: 'ds-hf-empty', hiddenFeatures: [] },
29
+ // filter
30
+ 'ds-combined': { id: 'ds-combined', filter: ['==', ['get', 'type'], 'foo'], hiddenFeatures: [5] }
31
+ })
32
+ })
33
+
34
+ describe('layerIds', () => {
35
+ it('returns [id] when dataset has a symbol (historic-monuments-prehistoric inherits symbol from parent)', () => {
36
+ const dataset = datasetRegistry.getDataset('historic-monuments-prehistoric')
37
+ expect(dataset.layerIds).toEqual(['historic-monuments-prehistoric'])
38
+ })
39
+
40
+ it('returns [id, id-stroke] when dataset has both fill and stroke (existing-fields)', () => {
41
+ const dataset = datasetRegistry.getDataset('existing-fields')
42
+ expect(dataset.layerIds).toEqual(['existing-fields', 'existing-fields-stroke'])
43
+ })
44
+
45
+ it('returns [id] when dataset has only fill', () => {
46
+ const dataset = datasetRegistry.getDataset('ds-fill-only')
47
+ expect(dataset.layerIds).toEqual(['ds-fill-only'])
48
+ })
49
+
50
+ it('returns [id] when dataset has only stroke (hedge-control)', () => {
51
+ const dataset = datasetRegistry.getDataset('hedge-control')
52
+ expect(dataset.layerIds).toEqual(['hedge-control'])
53
+ })
54
+
55
+ it('returns [id] when dataset has a fillPattern but no stroke', () => {
56
+ const dataset = datasetRegistry.getDataset('ds-pattern-only')
57
+ expect(dataset.layerIds).toEqual(['ds-pattern-only'])
58
+ })
59
+
60
+ it('returns [id, id-stroke] when dataset has fillPattern and stroke (land-covers-130-131)', () => {
61
+ const dataset = datasetRegistry.getDataset('land-covers-130-131')
62
+ expect(dataset.layerIds).toEqual(['land-covers-130-131', 'land-covers-130-131-stroke'])
63
+ })
64
+
65
+ it('returns null when fill is transparent and there is no stroke, symbol, or pattern', () => {
66
+ const dataset = datasetRegistry.getDataset('ds-transparent-fill')
67
+ expect(dataset.layerIds).toEqual([])
68
+ })
69
+
70
+ it('returns null when dataset has no fill, stroke, symbol, or pattern', () => {
71
+ const dataset = datasetRegistry.getDataset('ds-bare')
72
+ expect(dataset.layerIds).toEqual([])
73
+ })
74
+
75
+ it('returns the combined layerIds from all sublayers (historic-monuments)', () => {
76
+ const dataset = datasetRegistry.getDataset('historic-monuments')
77
+ expect(dataset.layerIds).toEqual([
78
+ 'historic-monuments-prehistoric',
79
+ 'historic-monuments-roman',
80
+ 'historic-monuments-medieval'
81
+ ])
82
+ })
83
+
84
+ it('returns the combined layerIds from all sublayers (land-covers)', () => {
85
+ const dataset = datasetRegistry.getDataset('land-covers')
86
+ expect(dataset.layerIds).toEqual([
87
+ 'land-covers-130-131', 'land-covers-130-131-stroke',
88
+ 'land-covers-332', 'land-covers-332-stroke',
89
+ 'land-covers-110', 'land-covers-110-stroke',
90
+ 'land-covers-379', 'land-covers-379-stroke',
91
+ 'land-covers-other', 'land-covers-other-stroke'
92
+ ])
93
+ })
94
+ })
95
+
96
+ describe('visibility', () => {
97
+ it('returns "visible" when visible is true', () => {
98
+ expect(datasetRegistry.getDataset('existing-fields').visibility).toBe('visible')
99
+ })
100
+
101
+ it('returns "none" when visible is false', () => {
102
+ expect(datasetRegistry.getDataset('hedge-control').visibility).toBe('none')
103
+ })
104
+ })
105
+
106
+ describe('getLayersWithVisibility', () => {
107
+ it('returns layerIds and visibility for a layer with sublayers', () => {
108
+ const registryDataset = datasetRegistry.getDataset('land-covers')
109
+ const layersWithVisibility = registryDataset.getLayersWithVisibility()
110
+ expect(layersWithVisibility).toEqual([
111
+ { layerIds: ['land-covers-130-131', 'land-covers-130-131-stroke'], visibility: 'visible' },
112
+ { layerIds: ['land-covers-332', 'land-covers-332-stroke'], visibility: 'visible' },
113
+ { layerIds: ['land-covers-110', 'land-covers-110-stroke'], visibility: 'visible' },
114
+ { layerIds: ['land-covers-379', 'land-covers-379-stroke'], visibility: 'none' },
115
+ { layerIds: ['land-covers-other', 'land-covers-other-stroke'], visibility: 'visible' }
116
+ ])
117
+ })
118
+ })
119
+
120
+ describe('getLayersWithOpacity', () => {
121
+ it('returns layerIds and opacity for a sublayer with no sublayers', () => {
122
+ const dataset = datasetRegistry.getDataset('existing-fields')
123
+ expect(dataset.getLayersWithOpacity()).toEqual([{ layerIds: ['existing-fields', 'existing-fields-stroke'], opacity: 1 }])
124
+ })
125
+
126
+ it('returns layerIds and opacity for a sublayer', () => {
127
+ const dataset = datasetRegistry.getDataset('land-covers-130-131')
128
+ const result = dataset.getLayersWithOpacity()
129
+ expect(result).toEqual([{ layerIds: ['land-covers-130-131', 'land-covers-130-131-stroke'], opacity: 1 }])
130
+ })
131
+
132
+ it('returns layerIds and opacity for a sublayer with sublayers with specific opacity', () => {
133
+ const parentDef = { id: 'parent-ds', sublayerIds: ['parent-ds-sub'] }
134
+ const subDef = { id: 'parent-ds-sub', parentId: 'parent-ds', style: { stroke: '#ff0000', fill: '#00ff00', opacity: 0.75 } }
135
+ datasetRegistry.attach({ 'parent-ds': parentDef, 'parent-ds-sub': subDef })
136
+ const dataset = datasetRegistry.getDataset('parent-ds')
137
+ const result = dataset.getLayersWithOpacity()
138
+ expect(result).toEqual([{ layerIds: ['parent-ds-sub', 'parent-ds-sub-stroke'], opacity: 0.75 }])
139
+ })
140
+
141
+ it('returns layerIds and opacity for all sublayers', () => {
142
+ const dataset = datasetRegistry.getDataset('historic-monuments')
143
+ expect(dataset.getLayersWithOpacity()).toEqual([
144
+ { layerIds: ['historic-monuments-prehistoric'], opacity: 1 },
145
+ { layerIds: ['historic-monuments-roman'], opacity: 1 },
146
+ { layerIds: ['historic-monuments-medieval'], opacity: 1 }
147
+ ])
148
+ })
149
+ })
150
+
151
+ describe('fillLayerId, strokeLayerId, symbolLayerId — hasSublayers returns null', () => {
152
+ it('fillLayerId returns null for a dataset with sublayers', () => {
153
+ const dataset = datasetRegistry.getDataset('land-covers')
154
+ expect(dataset.fillLayerId).toBeNull()
155
+ })
156
+
157
+ it('strokeLayerId returns null for a dataset with sublayers', () => {
158
+ const dataset = datasetRegistry.getDataset('land-covers')
159
+ expect(dataset.strokeLayerId).toBeNull()
160
+ })
161
+
162
+ it('symbolLayerId returns null for a dataset with sublayers', () => {
163
+ const dataset = datasetRegistry.getDataset('land-covers')
164
+ expect(dataset.symbolLayerId).toBeNull()
165
+ })
166
+ })
167
+
168
+ describe('getLayersWithFilters', () => {
169
+ it('returns an empty array when there are no hidden features and no sublayers', () => {
170
+ const dataset = datasetRegistry.getDataset('existing-fields')
171
+ expect(dataset.getLayersWithFilters()).toEqual([])
172
+ })
173
+
174
+ it('returns an entry with layerIds and filter when the dataset has hidden features', () => {
175
+ const dataset = datasetRegistry.getDataset('land-covers-130-131')
176
+ expect(dataset.getLayersWithFilters()).toEqual([{
177
+ layerIds: ['land-covers-130-131', 'land-covers-130-131-stroke'],
178
+ filter: ['all',
179
+ ['!', ['in', ['to-string', ['get', 'id']], ['literal', ['42']]]],
180
+ ['in', ['get', 'dominant_land_cover'], ['literal', ['130', '131']]]]
181
+ }])
182
+ })
183
+
184
+ it('includes sublayer entries when a sublayer has hidden features', () => {
185
+ const parentDef = { id: 'parent-ds', sublayerIds: ['parent-ds-sub'] }
186
+ const subDef = { id: 'parent-ds-sub', parentId: 'parent-ds', hiddenFeatures: [7], style: { stroke: '#ff0000', fill: '#00ff00' } }
187
+ datasetRegistry.attach({ 'parent-ds': parentDef, 'parent-ds-sub': subDef })
188
+ const dataset = datasetRegistry.getDataset('parent-ds')
189
+ expect(dataset.getLayersWithFilters()).toEqual([{
190
+ layerIds: ['parent-ds-sub', 'parent-ds-sub-stroke'],
191
+ filter: ['!', ['in', ['to-string', ['id']], ['literal', ['7']]]]
192
+ }])
193
+ })
194
+
195
+ it('returns an empty array when sublayers exist but none have hidden features', () => {
196
+ const dataset = datasetRegistry.getDataset('historic-monuments')
197
+ expect(dataset.getLayersWithFilters()).toEqual([])
198
+ })
199
+ })
200
+
201
+ describe('sourceId', () => {
202
+ it('returns the parent sourceId for a sublayer', () => {
203
+ const sublayer = datasetRegistry.getDataset('land-covers-130-131')
204
+ expect(sublayer.sourceId).toBe(datasetRegistry.getDataset('land-covers').sourceId)
205
+ })
206
+
207
+ it('returns a tiles-based id for a tile dataset (array tiles)', () => {
208
+ const dataset = datasetRegistry.getDataset('existing-fields')
209
+ expect(dataset.sourceId).toBe('tiles-3delfuv')
210
+ })
211
+
212
+ it('returns a tiles-based id when tiles is a plain string (line 76 non-array branch)', () => {
213
+ const dataset = datasetRegistry.getDataset('existing-fields')
214
+ expect(dataset.sourceId).toBe('tiles-3delfuv')
215
+ })
216
+
217
+ it('returns geojson-dynamic-{id} for a dynamic geojson source', () => {
218
+ expect(datasetRegistry.getDataset('land-covers').sourceId).toBe('geojson-dynamic-land-covers')
219
+ })
220
+
221
+ it('returns geojson-{hash} for a static string geojson url', () => {
222
+ expect(datasetRegistry.getDataset('ds-static-url').sourceId).toBe('geojson-1u4xay')
223
+ })
224
+
225
+ it('returns geojson-{id} for an object geojson source', () => {
226
+ const dataset = datasetRegistry.getDataset('historic-monuments')
227
+ expect(dataset.sourceId).toBe('geojson-historic-monuments')
228
+ })
229
+
230
+ it('returns source-{id} when there are no tiles and no geojson', () => {
231
+ expect(datasetRegistry.getDataset('ds-bare').sourceId).toBe('source-ds-bare')
232
+ })
233
+ })
234
+
235
+ describe('source', () => {
236
+ it('returns a vector source for a tile dataset', () => {
237
+ expect(datasetRegistry.getDataset('existing-fields').source).toEqual({
238
+ type: 'vector',
239
+ tiles: 'https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}',
240
+ minzoom: 10,
241
+ maxzoom: 24
242
+ })
243
+ })
244
+
245
+ it('falls back to 0/22 for minzoom/maxzoom when zoom is not set', () => {
246
+ expect(datasetRegistry.getDataset('ds-tiles-no-zoom').source).toEqual({
247
+ type: 'vector',
248
+ tiles: ['https://example.com/{z}/{x}/{y}'],
249
+ minzoom: 0,
250
+ maxzoom: 22
251
+ })
252
+ })
253
+
254
+ it('returns a geojson source with empty FeatureCollection for a dynamic geojson source', () => {
255
+ expect(datasetRegistry.getDataset('land-covers').source).toEqual({
256
+ type: 'geojson',
257
+ data: { type: 'FeatureCollection', features: [] },
258
+ generateId: true
259
+ })
260
+ })
261
+
262
+ it('returns a geojson source with the geojson data for an object geojson source', () => {
263
+ const dataset = datasetRegistry.getDataset('historic-monuments')
264
+ expect(dataset.source).toEqual({ type: 'geojson', data: dataset.geojson, generateId: true })
265
+ })
266
+
267
+ it('returns null when there are no tiles and no geojson', () => {
268
+ expect(datasetRegistry.getDataset('ds-bare').source).toBeNull()
269
+ })
270
+ })
271
+
272
+ describe('getSymbolSource', () => {
273
+ const prehistoricBase = {
274
+ id: 'historic-monuments-prehistoric',
275
+ type: 'symbol',
276
+ source: 'geojson-historic-monuments',
277
+ 'source-layer': undefined,
278
+ minzoom: 6, // datasetDefaults.minZoom applied to sublayer (parent's 10 is not inherited)
279
+ maxzoom: 24,
280
+ filter: ['in', ['get', 'category'], 'prehistoric']
281
+ }
282
+
283
+ it('returns a symbol layer spec with the correct shape', () => {
284
+ expect(datasetRegistry.getDataset('historic-monuments-prehistoric').getSymbolSource('my-icon', null, null)).toEqual({
285
+ ...prehistoricBase,
286
+ layout: { visibility: 'visible', 'icon-image': 'my-icon', 'icon-anchor': 'center', 'icon-allow-overlap': true }
287
+ })
288
+ })
289
+
290
+ it('uses the provided anchor when given', () => {
291
+ expect(datasetRegistry.getDataset('historic-monuments-prehistoric').getSymbolSource('icon', [0.1, 0.9], null)).toEqual({
292
+ ...prehistoricBase,
293
+ layout: { visibility: 'visible', 'icon-image': 'icon', 'icon-anchor': 'bottom-left', 'icon-allow-overlap': true }
294
+ })
295
+ })
296
+
297
+ it('falls back to symbolDef.anchor when no anchor is provided', () => {
298
+ expect(datasetRegistry.getDataset('historic-monuments-prehistoric').getSymbolSource('icon', null, { anchor: [0.5, 0] })).toEqual({
299
+ ...prehistoricBase,
300
+ layout: { visibility: 'visible', 'icon-image': 'icon', 'icon-anchor': 'top', 'icon-allow-overlap': true }
301
+ })
302
+ })
303
+
304
+ it('does not include a filter property when filter is null', () => {
305
+ expect(datasetRegistry.getDataset('historic-monuments').getSymbolSource('icon', null, null)).toEqual({
306
+ id: null,
307
+ type: 'symbol',
308
+ source: 'geojson-historic-monuments',
309
+ 'source-layer': undefined,
310
+ minzoom: 10,
311
+ maxzoom: 24,
312
+ layout: { visibility: 'visible', 'icon-image': 'icon', 'icon-anchor': 'center', 'icon-allow-overlap': true }
313
+ })
314
+ })
315
+
316
+ it('includes a filter property when the dataset has a filter', () => {
317
+ expect(datasetRegistry.getDataset('historic-monuments-prehistoric').getSymbolSource('icon', null, null)).toEqual({
318
+ ...prehistoricBase,
319
+ layout: { visibility: 'visible', 'icon-image': 'icon', 'icon-anchor': 'center', 'icon-allow-overlap': true }
320
+ })
321
+ })
322
+ })
323
+
324
+ describe('getFillSource', () => {
325
+ const fillBase = {
326
+ id: 'ds-fill-only',
327
+ type: 'fill',
328
+ source: 'source-ds-fill-only',
329
+ 'source-layer': undefined,
330
+ minzoom: 10,
331
+ maxzoom: 24,
332
+ layout: { visibility: 'visible' }
333
+ }
334
+
335
+ it('returns a fill layer spec with the correct shape', () => {
336
+ expect(datasetRegistry.getDataset('ds-fill-only').getFillSource({ 'fill-color': '#ff0000' })).toEqual({
337
+ ...fillBase,
338
+ paint: { 'fill-color': '#ff0000' }
339
+ })
340
+ })
341
+
342
+ it('does not include a filter property when filter is null', () => {
343
+ expect(datasetRegistry.getDataset('ds-fill-only').getFillSource({})).toEqual({ ...fillBase, paint: {} })
344
+ })
345
+
346
+ it('includes a filter property when the dataset has a filter', () => {
347
+ expect(datasetRegistry.getDataset('existing-fields').getFillSource({})).toEqual({
348
+ id: 'existing-fields',
349
+ type: 'fill',
350
+ source: 'tiles-3delfuv',
351
+ 'source-layer': 'field_parcels_filtered',
352
+ minzoom: 10,
353
+ maxzoom: 24,
354
+ layout: { visibility: 'visible' },
355
+ paint: {},
356
+ filter: ['all', ['==', ['get', 'sbi'], '106223377'], ['==', ['get', 'is_dominant_land_cover'], true]]
357
+ })
358
+ })
359
+ })
360
+
361
+ describe('getStrokeSource', () => {
362
+ it('returns a line layer spec with the correct shape', () => {
363
+ expect(datasetRegistry.getDataset('hedge-control')
364
+ .getStrokeSource({ 'line-color': '#ff0000', 'line-width': 2 }))
365
+ .toEqual({
366
+ id: 'hedge-control',
367
+ type: 'line',
368
+ source: 'tiles-3delfuv',
369
+ 'source-layer': 'hedge_control',
370
+ minzoom: 10,
371
+ maxzoom: 24,
372
+ layout: { visibility: 'none' },
373
+ paint: { 'line-color': '#ff0000', 'line-width': 2 }
374
+ })
375
+ })
376
+
377
+ it('includes a filter property when the dataset has a filter', () => {
378
+ expect(datasetRegistry.getDataset('existing-fields')
379
+ .getStrokeSource({}))
380
+ .toEqual({
381
+ id: 'existing-fields-stroke',
382
+ type: 'line',
383
+ source: 'tiles-3delfuv',
384
+ 'source-layer': 'field_parcels_filtered',
385
+ minzoom: 10,
386
+ maxzoom: 24,
387
+ layout: { visibility: 'visible' },
388
+ paint: {},
389
+ filter: ['all', ['==', ['get', 'sbi'], '106223377'], ['==', ['get', 'is_dominant_land_cover'], true]]
390
+ })
391
+ })
392
+ })
393
+
394
+ describe('_hiddenFeaturesIdExpression', () => {
395
+ it('uses get(idProperty) when idProperty is set', () => {
396
+ expect(datasetRegistry.getDataset('land-covers')._hiddenFeaturesIdExpression).toEqual(['to-string', ['get', 'id']])
397
+ })
398
+
399
+ it('uses the feature id when idProperty is not set', () => {
400
+ expect(datasetRegistry.getDataset('ds-bare')._hiddenFeaturesIdExpression).toEqual(['to-string', ['id']])
401
+ })
402
+ })
403
+
404
+ describe('filter', () => {
405
+ it('returns null when there is no filter and no hidden features', () => {
406
+ expect(datasetRegistry.getDataset('ds-bare').filter).toBeNull()
407
+ })
408
+
409
+ it('returns the own filter directly when it is the only filter', () => {
410
+ expect(datasetRegistry.getDataset('historic-monuments-prehistoric').filter)
411
+ .toEqual(['in', ['get', 'category'], 'prehistoric'])
412
+ })
413
+
414
+ it('returns ["all", parentFilter, ownFilter] when both parent and own filter are present', () => {
415
+ const parentFilter = ['==', ['get', 'cat'], 'a']
416
+ const childFilter = ['==', ['get', 'sub'], 'b']
417
+ const parentDef = { id: 'p-filter', filter: parentFilter, sublayerIds: ['p-filter-child'] }
418
+ const childDef = { id: 'p-filter-child', parentId: 'p-filter', filter: childFilter }
419
+ datasetRegistry.attach({ 'p-filter': parentDef, 'p-filter-child': childDef })
420
+ const dataset = datasetRegistry.getDataset('p-filter-child')
421
+ expect(dataset.filter).toEqual(['all', parentFilter, childFilter])
422
+ })
423
+
424
+ it('returns the hidden features filter directly when it is the only filter', () => {
425
+ expect(datasetRegistry.getDataset('ds-hf-123').filter)
426
+ .toEqual(['!', ['in', ['to-string', ['id']], ['literal', ['1', '2', '3']]]])
427
+ })
428
+
429
+ it('returns ["all", ...] when both own filter and hidden features filter are present', () => {
430
+ const { filter } = datasetRegistry.getDataset('ds-combined')
431
+ expect(filter).toEqual([
432
+ 'all',
433
+ ['==', ['get', 'type'], 'foo'],
434
+ [
435
+ '!',
436
+ ['in', ['to-string', ['id']], ['literal', ['5']]]
437
+ ]
438
+ ])
439
+ })
440
+ })
441
+ })
@@ -1,6 +1,3 @@
1
- import { datasetDefaults } from '../defaults.js'
2
-
3
- export const addDataset = ({ pluginState, mapState }, dataset) => {
4
- pluginState.layerAdapter?.addDataset({ ...datasetDefaults, ...dataset }, mapState.mapStyle)
5
- pluginState.dispatch({ type: 'ADD_DATASET', payload: { dataset, datasetDefaults } })
1
+ export const addDataset = ({ pluginState: { dispatch }, mapState }, dataset) => {
2
+ dispatch({ type: 'ADD_DATASET', payload: { dataset, mapStyle: mapState.mapStyle } })
6
3
  }
@@ -0,0 +1,16 @@
1
+ import { addDataset } from './addDataset.js'
2
+
3
+ describe('addDataset', () => {
4
+ it('dispatches ADD_DATASET with the dataset and mapStyle', () => {
5
+ const dispatch = jest.fn()
6
+ const dataset = { id: 'my-dataset', label: 'My Dataset' }
7
+ const mapState = { mapStyle: { version: 8, layers: [] } }
8
+
9
+ addDataset({ pluginState: { dispatch }, mapState }, dataset)
10
+
11
+ expect(dispatch).toHaveBeenCalledWith({
12
+ type: 'ADD_DATASET',
13
+ payload: { dataset, mapStyle: mapState.mapStyle }
14
+ })
15
+ })
16
+ })