@defra/interactive-map 0.0.30-alpha → 0.0.32-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 (247) hide show
  1. package/README.md +2 -2
  2. package/assets/images/bright-ofm-thumb.jpg +0 -0
  3. package/assets/images/dark-ofm-thumb.jpg +0 -0
  4. package/assets/images/road-ofm-thumb.jpg +0 -0
  5. package/assets/templates/add-polygons.njk +143 -0
  6. package/assets/templates/add-symbols.njk +119 -0
  7. package/assets/templates/{map.njk → basic-map.njk} +9 -17
  8. package/assets/templates/button-map.njk +56 -0
  9. package/assets/templates/draw-tools.njk +161 -0
  10. package/assets/templates/marker-panel.njk +91 -0
  11. package/assets/templates/place-marker.njk +104 -0
  12. package/assets/templates/search-control.njk +98 -0
  13. package/assets/templates/select-feature.njk +101 -0
  14. package/assets/templates/style-switcher.njk +87 -0
  15. package/assets/templates/toggle-marker-label.njk +74 -0
  16. package/dist/css/index.css +1 -1
  17. package/dist/esm/im-core.js +1 -1
  18. package/dist/esm/im-shell.js +1 -1
  19. package/dist/umd/im-core.js +1 -1
  20. package/dist/umd/index.js +1 -1
  21. package/docs/api/context.md +4 -4
  22. package/docs/api.md +166 -37
  23. package/docs/assets/css/docusaurus.scss +36 -4
  24. package/docs/assets/images/add-polygons.jpg +0 -0
  25. package/docs/assets/images/add-symbols.jpg +0 -0
  26. package/docs/assets/images/basic-map.jpg +0 -0
  27. package/docs/assets/images/button-first.jpg +0 -0
  28. package/docs/assets/images/deuteranopia-ozs-map-thumb.jpg +0 -0
  29. package/docs/assets/images/draw.jpg +0 -0
  30. package/docs/assets/images/marker-panel.jpg +0 -0
  31. package/docs/assets/images/night-ozs-map-thumb.jpg +0 -0
  32. package/docs/assets/images/outdoor-ozs-map-thumb.jpg +0 -0
  33. package/docs/assets/images/place-marker.jpg +0 -0
  34. package/docs/assets/images/screenshot.jpg +0 -0
  35. package/docs/assets/images/search.jpg +0 -0
  36. package/docs/assets/images/select-feature.jpg +0 -0
  37. package/docs/assets/images/style-switcher.jpg +0 -0
  38. package/docs/assets/images/toggle-marker-label.jpg +0 -0
  39. package/docs/assets/images/tritanopia-ozs-map-thumb.jpg +0 -0
  40. package/docs/examples/add-marker-with-panel.mdx +3 -3
  41. package/docs/examples/add-polygons.mdx +225 -0
  42. package/docs/examples/add-symbols.mdx +177 -0
  43. package/docs/examples/basic-map.mdx +2 -2
  44. package/docs/examples/draw-tools.mdx +271 -0
  45. package/docs/examples/index.mdx +95 -11
  46. package/docs/examples/place-marker.mdx +151 -0
  47. package/docs/examples/search-control.mdx +71 -0
  48. package/docs/examples/select-feature.mdx +146 -0
  49. package/docs/examples/style-switcher.mdx +135 -0
  50. package/docs/examples/toggle-marker-label.mdx +3 -3
  51. package/docs/plugins/datasets.md +157 -122
  52. package/docs/plugins/interact.md +1 -4
  53. package/docusaurus.config.cjs +9 -2
  54. package/govuk-prototype-kit.config.json +52 -2
  55. package/package.json +1 -1
  56. package/plugins/beta/datasets/dist/css/index.css +5 -2
  57. package/plugins/beta/datasets/dist/esm/im-datasets-ml-adapter.js +1 -0
  58. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  59. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  60. package/plugins/beta/datasets/dist/umd/im-datasets-ml-adapter.js +2 -0
  61. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -0
  62. package/plugins/beta/datasets/dist/umd/index.js +2 -0
  63. package/plugins/beta/datasets/src/adapters/loadLayerAdapter.js +33 -0
  64. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +44 -120
  65. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +145 -245
  66. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +516 -0
  67. package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +209 -0
  68. package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +483 -0
  69. package/plugins/beta/datasets/src/api/addDataset.js +2 -5
  70. package/plugins/beta/datasets/src/api/addDataset.test.js +16 -0
  71. package/plugins/beta/datasets/src/api/getOpacity.js +14 -14
  72. package/plugins/beta/datasets/src/api/getOpacity.test.js +72 -0
  73. package/plugins/beta/datasets/src/api/getStyle.js +8 -9
  74. package/plugins/beta/datasets/src/api/getStyle.test.js +42 -0
  75. package/plugins/beta/datasets/src/api/removeDataset.js +7 -7
  76. package/plugins/beta/datasets/src/api/removeDataset.test.js +45 -0
  77. package/plugins/beta/datasets/src/api/setData.js +9 -3
  78. package/plugins/beta/datasets/src/api/setData.test.js +62 -0
  79. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +5 -30
  80. package/plugins/beta/datasets/src/api/setDatasetVisibility.test.js +31 -0
  81. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +9 -18
  82. package/plugins/beta/datasets/src/api/setFeatureVisibility.test.js +74 -0
  83. package/plugins/beta/datasets/src/api/setGlobals.js +20 -0
  84. package/plugins/beta/datasets/src/api/setGlobals.test.js +63 -0
  85. package/plugins/beta/datasets/src/api/setOpacity.js +8 -27
  86. package/plugins/beta/datasets/src/api/setOpacity.test.js +31 -0
  87. package/plugins/beta/datasets/src/api/setStyle.js +5 -21
  88. package/plugins/beta/datasets/src/api/setStyle.test.js +53 -0
  89. package/plugins/beta/datasets/src/components/Key/Key.jsx +51 -0
  90. package/plugins/beta/datasets/src/components/Key/KeyGroupItem.jsx +18 -0
  91. package/plugins/beta/datasets/src/components/Key/KeyItem.jsx +20 -0
  92. package/plugins/beta/datasets/src/components/Key/KeySvg.jsx +22 -0
  93. package/plugins/beta/datasets/src/components/Key/KeySvg.test.jsx +87 -0
  94. package/plugins/beta/datasets/src/components/Key/KeySvgLine.jsx +19 -0
  95. package/plugins/beta/datasets/src/components/Key/KeySvgLine.test.jsx +71 -0
  96. package/plugins/beta/datasets/src/components/{KeySvgPattern.jsx → Key/KeySvgPattern.jsx} +3 -4
  97. package/plugins/beta/datasets/src/components/Key/KeySvgPattern.test.jsx +55 -0
  98. package/plugins/beta/datasets/src/components/Key/KeySvgRect.jsx +22 -0
  99. package/plugins/beta/datasets/src/components/Key/KeySvgRect.test.jsx +85 -0
  100. package/plugins/beta/datasets/src/components/{KeySvgSymbol.jsx → Key/KeySvgSymbol.jsx} +5 -5
  101. package/plugins/beta/datasets/src/components/Key/KeySvgSymbol.test.jsx +80 -0
  102. package/plugins/beta/datasets/src/components/Key/svgProperties.test.js +61 -0
  103. package/plugins/beta/datasets/src/{panels → components/LayersMenu}/Layers.module.scss +15 -11
  104. package/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx +33 -0
  105. package/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx +31 -0
  106. package/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx +19 -0
  107. package/plugins/beta/datasets/src/datasets.scss +2 -2
  108. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +8 -8
  109. package/plugins/beta/datasets/src/{DatasetsInit.jsx → initialise/DatasetsInit.jsx} +19 -21
  110. package/plugins/beta/datasets/src/{defaults.js → initialise/defaults.js} +17 -2
  111. package/plugins/beta/datasets/src/initialise/defaults.test.js +127 -0
  112. package/plugins/beta/datasets/src/{datasets.js → initialise/initialiseDatasets.js} +30 -26
  113. package/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js +273 -0
  114. package/plugins/beta/datasets/src/manifest.js +8 -6
  115. package/plugins/beta/datasets/src/reducers/__data__/demoDatasets.js +253 -0
  116. package/plugins/beta/datasets/src/reducers/datasetsToMenu.js +83 -0
  117. package/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js +84 -0
  118. package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.js +41 -0
  119. package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.test.js +117 -0
  120. package/plugins/beta/datasets/src/reducers/pluginState.js +206 -0
  121. package/plugins/beta/datasets/src/reducers/pluginState.test.js +386 -0
  122. package/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js +29 -0
  123. package/plugins/beta/datasets/src/registry/createDataset.js +5 -0
  124. package/plugins/beta/datasets/src/registry/dataset.js +176 -0
  125. package/plugins/beta/datasets/src/registry/dataset.test.js +497 -0
  126. package/plugins/beta/datasets/src/registry/datasetDefinitionCache.js +35 -0
  127. package/plugins/beta/datasets/src/registry/datasetRegistry.js +116 -0
  128. package/plugins/beta/datasets/src/registry/datasetRegistry.test.js +335 -0
  129. package/plugins/beta/datasets/src/registry/dynamicGeoJson.js +17 -0
  130. package/plugins/beta/datasets/src/registry/globalDataset.js +36 -0
  131. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  132. package/plugins/beta/draw-es/src/api/addFeature.js +12 -3
  133. package/plugins/beta/draw-es/src/events.js +8 -3
  134. package/plugins/beta/draw-es/src/events.test.js +2 -0
  135. package/plugins/beta/draw-es/src/graphic.js +26 -2
  136. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  137. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  138. package/plugins/beta/draw-ml/src/DrawInit.jsx +8 -0
  139. package/plugins/beta/draw-ml/src/api/newLine.js +1 -0
  140. package/plugins/beta/draw-ml/src/api/newPolygon.js +1 -0
  141. package/plugins/beta/draw-ml/src/events.js +13 -1
  142. package/plugins/beta/draw-ml/src/index.js +1 -1
  143. package/plugins/beta/draw-ml/src/manifest.js +3 -0
  144. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +21 -12
  145. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +4 -0
  146. package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -1
  147. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  148. package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +4 -2
  149. package/plugins/beta/map-styles/src/mapStyles.scss +41 -41
  150. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  151. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  152. package/plugins/interact/dist/umd/index.js +1 -1
  153. package/plugins/interact/src/InteractInit.jsx +3 -5
  154. package/plugins/interact/src/InteractInit.test.js +1 -3
  155. package/plugins/interact/src/events.js +2 -29
  156. package/plugins/interact/src/events.test.js +4 -102
  157. package/plugins/interact/src/hooks/useAttachEvents.js +3 -4
  158. package/plugins/interact/src/hooks/useAttachEvents.test.js +1 -3
  159. package/plugins/interact/src/hooks/useCrossHairVisibility.js +4 -10
  160. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +5 -10
  161. package/plugins/interact/src/hooks/useHighlightSync.js +1 -10
  162. package/plugins/interact/src/hooks/useHighlightSync.test.js +2 -21
  163. package/plugins/interact/src/hooks/useInteractionHandlers.js +15 -12
  164. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +28 -29
  165. package/plugins/interact/src/manifest.js +0 -35
  166. package/plugins/interact/src/manifest.test.js +3 -43
  167. package/plugins/interact/src/reducer.js +4 -22
  168. package/plugins/interact/src/reducer.test.js +11 -39
  169. package/plugins/search/dist/css/index.css +5 -2
  170. package/plugins/search/src/components/Form/Form.module.scss +5 -2
  171. package/plugins/search/src/search.scss +4 -4
  172. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  173. package/providers/beta/esri/src/esriProvider.js +4 -0
  174. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  175. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  176. package/providers/beta/openlayers/src/openlayersProvider.js +4 -0
  177. package/providers/beta/openlayers/src/openlayersProvider.test.js +5 -0
  178. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  179. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  180. package/providers/maplibre/src/maplibreProvider.js +6 -4
  181. package/providers/maplibre/src/maplibreProvider.test.js +5 -0
  182. package/providers/maplibre/src/utils/highlightFeatures.js +74 -25
  183. package/providers/maplibre/src/utils/highlightFeatures.test.js +132 -0
  184. package/providers/maplibre/src/utils/patternImages.js +2 -4
  185. package/providers/maplibre/src/utils/symbolImages.js +1 -4
  186. package/rollup.esm.mjs +2 -8
  187. package/src/App/components/CrossHair/CrossHair.module.scss +8 -8
  188. package/src/App/components/MapButton/MapButton.jsx +16 -1
  189. package/src/App/components/MapButton/MapButton.module.scss +12 -1
  190. package/src/App/components/MapButton/MapButton.test.jsx +40 -1
  191. package/src/App/hooks/useContinueEnabledEvaluator.js +31 -0
  192. package/src/App/hooks/useContinueEnabledEvaluator.test.js +118 -0
  193. package/src/App/hooks/useEvaluateProp.js +15 -15
  194. package/src/App/hooks/useEvaluateProp.test.js +80 -76
  195. package/src/App/hooks/useLayoutMeasurements.js +11 -5
  196. package/src/App/hooks/useResizeObserver.js +15 -2
  197. package/src/App/hooks/useResizeObserver.test.js +1 -0
  198. package/src/App/renderer/PluginInits.jsx +2 -0
  199. package/src/App/renderer/mapButtons.js +21 -1
  200. package/src/App/renderer/mapButtons.test.js +46 -1
  201. package/src/App/store/appReducer.js +1 -1
  202. package/src/App/store/appReducer.test.js +37 -0
  203. package/src/InteractiveMap/InteractiveMap.js +14 -4
  204. package/src/InteractiveMap/InteractiveMap.test.js +11 -3
  205. package/src/InteractiveMap/behaviourController.js +3 -1
  206. package/src/InteractiveMap/behaviourController.test.js +6 -3
  207. package/src/InteractiveMap/domStateManager.js +2 -0
  208. package/src/config/appConfig.js +25 -0
  209. package/src/config/appConfig.test.js +50 -0
  210. package/src/config/defaults.js +1 -0
  211. package/src/config/events.js +21 -3
  212. package/src/scss/settings/_dimensions.scss +1 -0
  213. package/src/services/patternRegistry.js +8 -4
  214. package/src/types.js +7 -0
  215. package/src/utils/patternUtils.js +1 -2
  216. package/src/utils/patternUtils.test.js +9 -0
  217. package/webpack.dev.mjs +10 -0
  218. package/webpack.umd.mjs +1 -1
  219. package/docs/assets/images/screens-blue.jpg +0 -0
  220. package/docs/assets/images/screens-white.jpg +0 -0
  221. package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +0 -1
  222. package/plugins/beta/datasets/dist/adapters/maplibre/esm/index.js +0 -1
  223. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +0 -2
  224. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +0 -2
  225. package/plugins/beta/datasets/dist/umd/maplibre/index.js +0 -2
  226. package/plugins/beta/datasets/src/adapters/maplibre/index.js +0 -18
  227. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +0 -75
  228. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +0 -27
  229. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +0 -31
  230. package/plugins/beta/datasets/src/components/KeySvg.jsx +0 -24
  231. package/plugins/beta/datasets/src/components/KeySvgLine.jsx +0 -19
  232. package/plugins/beta/datasets/src/components/KeySvgRect.jsx +0 -22
  233. package/plugins/beta/datasets/src/index.maplibre.umd.js +0 -23
  234. package/plugins/beta/datasets/src/panels/Key.jsx +0 -62
  235. package/plugins/beta/datasets/src/panels/Layers.jsx +0 -141
  236. package/plugins/beta/datasets/src/reducer.js +0 -219
  237. package/plugins/beta/datasets/src/reducers/keyReducer.js +0 -34
  238. package/plugins/beta/datasets/src/utils/filters.js +0 -34
  239. package/plugins/beta/datasets/src/utils/mergeSublayer.js +0 -86
  240. package/plugins/beta/draw-ml/dist/css/index.css +0 -21
  241. /package/plugins/beta/datasets/dist/umd/{maplibre/im-datasets-maplibre-adapter.js.LICENSE.txt → im-datasets-ml-adapter.js.LICENSE.txt} +0 -0
  242. /package/plugins/beta/datasets/dist/umd/{maplibre/im-datasets-plugin.js.LICENSE.txt → im-datasets-plugin.js.LICENSE.txt} +0 -0
  243. /package/plugins/beta/datasets/dist/umd/{maplibre/index.js.LICENSE.txt → index.js.LICENSE.txt} +0 -0
  244. /package/plugins/beta/datasets/src/components/{EmptyKey.jsx → Key/EmptyKey.jsx} +0 -0
  245. /package/plugins/beta/datasets/src/components/{EmptyKey.test.jsx → Key/EmptyKey.test.jsx} +0 -0
  246. /package/plugins/beta/datasets/src/{panels → components/Key}/Key.module.scss +0 -0
  247. /package/plugins/beta/datasets/src/components/{svgProperties.js → Key/svgProperties.js} +0 -0
@@ -0,0 +1,497 @@
1
+ import { Dataset } from './dataset.js'
2
+ import { datasetRegistry } from './datasetRegistry.js'
3
+ import { attachGlobalState } from './globalDataset.js'
4
+ // Use the mock datasetRegistry with the demo datasets attached before each test
5
+ // so we can test Dataset methods that depend on parent/sublayer relationships and styles
6
+ jest.mock('./datasetRegistry.js')
7
+
8
+ const globalState = {
9
+ opacityMode: 'dataset',
10
+ opacity: 1,
11
+ visible: true
12
+ }
13
+ describe('Dataset class', () => {
14
+ describe('isSublayer', () => {
15
+ it('returns false for a top-level dataset', () => {
16
+ const dataset = datasetRegistry.getDataset('land-covers')
17
+ expect(dataset.isSublayer).toBe(false)
18
+ })
19
+
20
+ it('returns true for a sublayer', () => {
21
+ const dataset = datasetRegistry.getDataset('land-covers-130-131')
22
+ expect(dataset.isSublayer).toBe(true)
23
+ })
24
+ })
25
+
26
+ describe('hasSublayers', () => {
27
+ it('returns true for a dataset that has sublayers', () => {
28
+ const dataset = datasetRegistry.getDataset('land-covers')
29
+ expect(dataset.hasSublayers).toBe(true)
30
+ })
31
+
32
+ it('returns false for a dataset with no sublayers', () => {
33
+ const dataset = datasetRegistry.getDataset('hedge-control')
34
+ expect(dataset.hasSublayers).toBe(false)
35
+ })
36
+
37
+ it('returns false for a dataset with an empty sublayerIds array', () => {
38
+ const dataset = new Dataset({ sublayerIds: [] })
39
+ expect(dataset.hasSublayers).toBe(false)
40
+ })
41
+ })
42
+
43
+ describe('sublayers', () => {
44
+ it('returns undefined for a dataset with no sublayerIds', () => {
45
+ const dataset = datasetRegistry.getDataset('hedge-control')
46
+ expect(dataset.sublayers).toBeUndefined()
47
+ })
48
+
49
+ it('returns a Dataset instance for each sublayer', () => {
50
+ const dataset = datasetRegistry.getDataset('land-covers')
51
+ expect(dataset.sublayers).toHaveLength(5)
52
+ dataset.sublayers.forEach(s => expect(s).toBeInstanceOf(Dataset))
53
+ })
54
+
55
+ it('returns sublayers for historic-monuments', () => {
56
+ const dataset = datasetRegistry.getDataset('historic-monuments')
57
+ expect(dataset.sublayers).toHaveLength(3)
58
+ dataset.sublayers.forEach(s => expect(s).toBeInstanceOf(Dataset))
59
+ })
60
+ })
61
+
62
+ describe('parent', () => {
63
+ it('returns undefined for a top-level dataset', () => {
64
+ const dataset = datasetRegistry.getDataset('land-covers')
65
+ expect(dataset.parent).toBeUndefined()
66
+ })
67
+
68
+ it('returns a Dataset instance representing the parent for a sublayer', () => {
69
+ const dataset = datasetRegistry.getDataset('land-covers-130-131')
70
+ expect(dataset.parent).toBeInstanceOf(Dataset)
71
+ })
72
+ })
73
+
74
+ describe('style', () => {
75
+ it('returns the dataset style directly when there is no parent', () => {
76
+ const dataset = new Dataset({ style: { stroke: '#ff0000', fill: 'transparent' } })
77
+ expect(dataset.style).toEqual({ stroke: '#ff0000', fill: 'transparent' })
78
+ })
79
+
80
+ it('merges parent style with the sublayer style', () => {
81
+ const parentDef = { id: 'parent', style: { stroke: '#ff0000', strokeWidth: 2 } }
82
+ const childDef = { id: 'child', parentId: 'parent', style: { fill: 'blue' } }
83
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
84
+
85
+ const dataset = new Dataset(childDef)
86
+ expect(dataset.style).toMatchObject({ stroke: '#ff0000', strokeWidth: 2, fill: 'blue' })
87
+ })
88
+
89
+ it('overrides parent style properties with the sublayer own style', () => {
90
+ const parentDef = { id: 'parent', style: { stroke: '#ff0000' } }
91
+ const childDef = { id: 'child', parentId: 'parent', style: { stroke: '#00ff00' } }
92
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
93
+
94
+ const dataset = new Dataset(childDef)
95
+ expect(dataset.style.stroke).toBe('#00ff00')
96
+ })
97
+
98
+ it('includes symbolDescription in the merged sublayer style', () => {
99
+ const parentDef = { id: 'parent', style: { stroke: '#ff0000' } }
100
+ const childDef = { id: 'child', parentId: 'parent', style: { symbolDescription: 'custom' } }
101
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
102
+
103
+ const dataset = new Dataset(childDef)
104
+ expect(dataset.style.symbolDescription).toBe('custom')
105
+ })
106
+ })
107
+
108
+ describe('hasCustomVisualStyle', () => {
109
+ it.each([
110
+ ['stroke', { stroke: '#ff0000' }],
111
+ ['fill', { fill: 'transparent' }],
112
+ ['fillPattern', { fillPattern: 'dots' }],
113
+ ['fillPatternSvgContent', { fillPatternSvgContent: '<svg/>' }],
114
+ ['symbol', { symbol: 'square' }],
115
+ ['symbolSvgContent', { symbolSvgContent: '<svg/>' }]
116
+ ])('returns true when style contains %s', (_, style) => {
117
+ expect(new Dataset({ style }).hasCustomVisualStyle).toBe(true)
118
+ })
119
+
120
+ it('returns false when style contains no visual style properties', () => {
121
+ const dataset = new Dataset({ style: { symbolBackgroundColor: '#ff0000', strokeWidth: 3 } })
122
+ expect(dataset.hasCustomVisualStyle).toBe(false)
123
+ })
124
+
125
+ it('returns false when there is no style', () => {
126
+ const dataset = new Dataset({})
127
+ expect(dataset.hasCustomVisualStyle).toBe(false)
128
+ })
129
+ })
130
+
131
+ describe('symbolDescription', () => {
132
+ it("returns the dataset's own symbolDescription", () => {
133
+ const dataset = new Dataset({ style: { symbolDescription: 'a circle' } })
134
+ expect(dataset.symbolDescription).toBe('a circle')
135
+ })
136
+
137
+ it('returns undefined when the dataset has a custom visual style but no symbolDescription', () => {
138
+ const dataset = new Dataset({ style: { stroke: '#ff0000' } })
139
+ expect(dataset.symbolDescription).toBeUndefined()
140
+ })
141
+
142
+ it('returns undefined when the dataset has no style', () => {
143
+ const dataset = new Dataset({})
144
+ expect(dataset.symbolDescription).toBeUndefined()
145
+ })
146
+
147
+ it("inherits the parent's symbolDescription when the sublayer has no custom visual style", () => {
148
+ const parentDef = { id: 'parent', style: { symbolDescription: 'parent symbol' } }
149
+ const childDef = { id: 'child', parentId: 'parent', style: {} }
150
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
151
+
152
+ const dataset = new Dataset(childDef)
153
+ expect(dataset.symbolDescription).toBe('parent symbol')
154
+ })
155
+
156
+ it('returns undefined when neither the sublayer nor the parent has a symbolDescription', () => {
157
+ const parentDef = { id: 'parent', style: { stroke: '#ff0000' } }
158
+ const childDef = { id: 'child', parentId: 'parent', style: {} }
159
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
160
+
161
+ // parent has a custom visual style (stroke) → parent.symbolDescription = undefined
162
+ // child has no visual style → inherits undefined from parent
163
+ const dataset = new Dataset(childDef)
164
+ expect(dataset.symbolDescription).toBeUndefined()
165
+ })
166
+ })
167
+
168
+ describe('label', () => {
169
+ it('returns the dataset label', () => {
170
+ const dataset = new Dataset({ label: 'My dataset' })
171
+ expect(dataset.label).toBe('My dataset')
172
+ })
173
+
174
+ it('returns undefined when no label is set', () => {
175
+ const dataset = new Dataset({})
176
+ expect(dataset.label).toBeUndefined()
177
+ })
178
+ })
179
+
180
+ describe('showInKey', () => {
181
+ it('returns true when showInKey is set on the dataset', () => {
182
+ const dataset = new Dataset({ showInKey: true })
183
+ expect(dataset.showInKey).toBe(true)
184
+ })
185
+
186
+ it('returns false when showInKey is not set and there is no parent', () => {
187
+ const dataset = new Dataset({})
188
+ expect(dataset.showInKey).toBe(false)
189
+ })
190
+
191
+ it('inherits showInKey from the parent when not set on the sublayer', () => {
192
+ const parentDef = { id: 'parent', showInKey: true, style: {} }
193
+ const childDef = { id: 'child', parentId: 'parent', style: {} }
194
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
195
+ expect(new Dataset(childDef).showInKey).toBe(true)
196
+ })
197
+
198
+ it('returns false when neither the sublayer nor the parent has showInKey set', () => {
199
+ const parentDef = { id: 'parent', style: {} }
200
+ const childDef = { id: 'child', parentId: 'parent', style: {} }
201
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
202
+ expect(new Dataset(childDef).showInKey).toBe(false)
203
+ })
204
+
205
+ it('allows a sublayer to opt out with showInKey: false even when parent has showInKey: true', () => {
206
+ const parentDef = { id: 'parent', showInKey: true, style: {} }
207
+ const childDef = { id: 'child', parentId: 'parent', showInKey: false, style: {} }
208
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
209
+ expect(new Dataset(childDef).showInKey).toBe(false)
210
+ })
211
+
212
+ it('allows a sublayer to opt in with showInKey: true even when parent has showInKey: false', () => {
213
+ const parentDef = { id: 'parent', showInKey: false, style: {} }
214
+ const childDef = { id: 'child', parentId: 'parent', showInKey: true, style: {} }
215
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
216
+ expect(new Dataset(childDef).showInKey).toBe(true)
217
+ })
218
+ })
219
+
220
+ describe('tiles, geojson, idProperty, parentId', () => {
221
+ it('returns tiles from the definition', () => {
222
+ const tiles = ['https://example.com/{z}/{x}/{y}']
223
+ const dataset = new Dataset({ tiles })
224
+ expect(dataset.tiles).toBe(tiles)
225
+ })
226
+
227
+ it('returns geojson from the definition', () => {
228
+ const geojson = { type: 'FeatureCollection', features: [] }
229
+ const dataset = new Dataset({ geojson })
230
+ expect(dataset.geojson).toBe(geojson)
231
+ })
232
+
233
+ it('returns idProperty from the definition', () => {
234
+ const dataset = new Dataset({ idProperty: 'gid' })
235
+ expect(dataset.idProperty).toBe('gid')
236
+ })
237
+
238
+ it('returns parentId from the definition', () => {
239
+ const dataset = new Dataset({ parentId: 'parent' })
240
+ expect(dataset.parentId).toBe('parent')
241
+ })
242
+ })
243
+
244
+ describe('minZoom / maxZoom', () => {
245
+ it('returns minZoom from the definition', () => {
246
+ const dataset = new Dataset({ minZoom: 8 })
247
+ expect(dataset.minZoom).toBe(8)
248
+ })
249
+
250
+ it('falls back to the parent minZoom when not set on the sublayer', () => {
251
+ const parentDef = { id: 'parent', minZoom: 10, style: {} }
252
+ const childDef = { id: 'child', parentId: 'parent', style: {} }
253
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
254
+ expect(new Dataset(childDef).minZoom).toBe(10)
255
+ })
256
+
257
+ it('returns maxZoom from the definition', () => {
258
+ const dataset = new Dataset({ maxZoom: 20 })
259
+ expect(dataset.maxZoom).toBe(20)
260
+ })
261
+
262
+ it('falls back to the parent maxZoom when not set on the sublayer', () => {
263
+ const parentDef = { id: 'parent', maxZoom: 22, style: {} }
264
+ const childDef = { id: 'child', parentId: 'parent', style: {} }
265
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
266
+ expect(new Dataset(childDef).maxZoom).toBe(22)
267
+ })
268
+ })
269
+
270
+ describe('opacity', () => {
271
+ const noOpacity = {
272
+ id: 'noOpacity'
273
+ }
274
+ const noOpacityChild = {
275
+ id: 'noOpacityChild',
276
+ parentId: 'noOpacity'
277
+ }
278
+ const opacityChild4 = {
279
+ id: 'opacityChild4',
280
+ parentId: 'noOpacity',
281
+ style: { opacity: 0.4 }
282
+ }
283
+ const zeroOpacity = {
284
+ id: 'zeroOpacity',
285
+ style: { opacity: 0 }
286
+ }
287
+ const opacity8 = {
288
+ id: 'opacity8',
289
+ style: { opacity: 0.8 }
290
+ }
291
+ const opacity8Child = {
292
+ id: 'opacity8Child',
293
+ parentId: 'opacity8',
294
+ style: { opacity: 0.4 }
295
+ }
296
+ const opacity8ChildNoOpacity = {
297
+ id: 'opacity8ChildNoOpacity',
298
+ parentId: 'opacity8'
299
+ }
300
+ beforeEach(() => {
301
+ datasetRegistry.attach({
302
+ zeroOpacity,
303
+ noOpacity,
304
+ noOpacityChild,
305
+ opacity8,
306
+ opacity8Child,
307
+ opacity8ChildNoOpacity,
308
+ opacityChild4
309
+ })
310
+ })
311
+
312
+ describe('with opacityMode set to "dataset"', () => {
313
+ it('returns global opacity when no opacity', () => {
314
+ expect(datasetRegistry.getDataset('noOpacity').opacity).toBe(1)
315
+ })
316
+
317
+ it('returns global opacity when no opacity is set in child or parent', () => {
318
+ expect(datasetRegistry.getDataset('noOpacityChild').opacity).toBe(1)
319
+ })
320
+
321
+ it('returns the specified opacity when opacity is set in child only', () => {
322
+ expect(datasetRegistry.getDataset('opacityChild4').opacity).toBe(0.4)
323
+ })
324
+
325
+ it('returns the specified opacity for a topLevel Dataset', () => {
326
+ expect(datasetRegistry.getDataset('opacity8').opacity).toBe(0.8)
327
+ })
328
+
329
+ it('returns the specified opacity when opacity is set in parent only', () => {
330
+ expect(datasetRegistry.getDataset('opacity8ChildNoOpacity').opacity).toBe(0.8)
331
+ })
332
+
333
+ it('returns the specified opacity for a child Dataset', () => {
334
+ expect(datasetRegistry.getDataset('opacity8Child').opacity).toBe(0.4)
335
+ })
336
+
337
+ it('returns 0 when opacity is explicitly set to 0', () => {
338
+ expect(datasetRegistry.getDataset('zeroOpacity').opacity).toBe(0)
339
+ })
340
+ })
341
+
342
+ describe('with opacityMode set to "multiply"', () => {
343
+ beforeEach(() => {
344
+ attachGlobalState({
345
+ ...globalState,
346
+ opacityMode: 'multiply',
347
+ opacity: 0.75
348
+ })
349
+ })
350
+
351
+ it('returns global opacity when no opacity', () => {
352
+ expect(datasetRegistry.getDataset('noOpacity').opacity).toBe(0.75)
353
+ })
354
+
355
+ it('returns global opacity when no opacity is set in child or parent', () => {
356
+ expect(datasetRegistry.getDataset('noOpacityChild').opacity).toBe(0.75)
357
+ })
358
+
359
+ it('returns the multiplied opacity when opacity is set in child only', () => {
360
+ expect(datasetRegistry.getDataset('opacityChild4').opacity).toBe(0.3)
361
+ })
362
+
363
+ it('returns the multiplied opacity for a topLevel Dataset', () => {
364
+ // 0.8 (dataset opacity) * 0.75 (global opacity)
365
+ expect(datasetRegistry.getDataset('opacity8').opacity).toBe(0.6)
366
+ })
367
+
368
+ it('returns the multiplied opacity for a child Dataset', () => {
369
+ // 0.4 (dataset opacity) * 0.75 (global opacity) * 0.8 (parent dataset opacity)
370
+ expect(datasetRegistry.getDataset('opacity8Child').opacity).toBe(0.24)
371
+ })
372
+
373
+ it('returns 0 when opacity is explicitly set to 0', () => {
374
+ expect(datasetRegistry.getDataset('zeroOpacity').opacity).toBe(0)
375
+ })
376
+ })
377
+
378
+ describe('with opacityMode set to "global"', () => {
379
+ beforeEach(() => {
380
+ attachGlobalState({
381
+ ...globalState,
382
+ opacityMode: 'global',
383
+ opacity: 0.6
384
+ })
385
+ })
386
+
387
+ it('returns the global opacity when no opacity', () => {
388
+ expect(datasetRegistry.getDataset('noOpacity').opacity).toBe(0.6)
389
+ })
390
+
391
+ it('returns the global opacity when no opacity is set in child or parent', () => {
392
+ expect(datasetRegistry.getDataset('noOpacityChild').opacity).toBe(0.6)
393
+ })
394
+
395
+ it('returns the global opacity for a topLevel Dataset', () => {
396
+ expect(datasetRegistry.getDataset('opacity8').opacity).toBe(0.6)
397
+ })
398
+
399
+ it('returns the global opacity for a child Dataset', () => {
400
+ expect(datasetRegistry.getDataset('opacity8Child').opacity).toBe(0.6)
401
+ })
402
+
403
+ it('returns global opacity when opacity is explicitly set to 0', () => {
404
+ expect(datasetRegistry.getDataset('zeroOpacity').opacity).toBe(0.6)
405
+ })
406
+ })
407
+ })
408
+
409
+ describe('hasDynamicGeoJSON', () => {
410
+ it('returns true when definition has a dynamicGeoJSON object', () => {
411
+ expect(datasetRegistry.getDataset('land-covers').hasDynamicGeoJSON).toBe(true)
412
+ })
413
+
414
+ it('returns false when definition does not have a dynamicGeoJSON object', () => {
415
+ expect(datasetRegistry.getDataset('historic-monuments').hasDynamicGeoJSON).toBe(false)
416
+ })
417
+ })
418
+
419
+ describe('hiddenFeatures / hasHiddenFeatures', () => {
420
+ it('returns hiddenFeatures from the definition', () => {
421
+ const dataset = new Dataset({ hiddenFeatures: [1, 2] })
422
+ expect(dataset.hiddenFeatures).toEqual([1, 2])
423
+ })
424
+
425
+ it('returns undefined when hiddenFeatures is not set', () => {
426
+ const dataset = new Dataset({})
427
+ expect(dataset.hiddenFeatures).toBeUndefined()
428
+ })
429
+
430
+ it('returns true from hasHiddenFeatures when hiddenFeatures is a non-empty array', () => {
431
+ const dataset = new Dataset({ hiddenFeatures: [42] })
432
+ expect(dataset.hasHiddenFeatures).toBe(true)
433
+ })
434
+
435
+ it('returns false from hasHiddenFeatures when hiddenFeatures is empty', () => {
436
+ const dataset = new Dataset({ hiddenFeatures: [] })
437
+ expect(dataset.hasHiddenFeatures).toBe(false)
438
+ })
439
+
440
+ it('returns false from hasHiddenFeatures when hiddenFeatures is not set', () => {
441
+ const dataset = new Dataset({})
442
+ expect(dataset.hasHiddenFeatures).toBe(false)
443
+ })
444
+
445
+ it('returns true from hasHiddenFeatures when the parent has hidden features', () => {
446
+ const parentDef = { id: 'parent', hiddenFeatures: [7], style: {} }
447
+ const childDef = { id: 'child', parentId: 'parent', style: {} }
448
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
449
+ expect(new Dataset(childDef).hasHiddenFeatures).toBe(true)
450
+ })
451
+ })
452
+
453
+ describe('filter', () => {
454
+ it('returns null', () => {
455
+ const dataset = new Dataset({ filter: ['==', ['get', 'type'], 'foo'] })
456
+ expect(dataset.filter).toBeNull()
457
+ })
458
+ })
459
+
460
+ describe('symbolAnchor', () => {
461
+ it('returns symbolAnchor from its own style', () => {
462
+ const dataset = new Dataset({ style: { symbolAnchor: [0.5, 1] } })
463
+ expect(dataset.symbolAnchor).toEqual([0.5, 1])
464
+ })
465
+
466
+ it('returns undefined when no symbolAnchor is set and there is no parent', () => {
467
+ const dataset = new Dataset({ style: {} })
468
+ expect(dataset.symbolAnchor).toBeUndefined()
469
+ })
470
+
471
+ it('inherits symbolAnchor from the parent when not set on the sublayer', () => {
472
+ const parentDef = { id: 'parent', style: { symbolAnchor: [0.1, 0.9] } }
473
+ const childDef = { id: 'child', parentId: 'parent', style: {} }
474
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
475
+ expect(new Dataset(childDef).symbolAnchor).toEqual([0.1, 0.9])
476
+ })
477
+ })
478
+
479
+ describe('sourceLayer', () => {
480
+ it('returns the sourceLayer from a tile-based top-level dataset', () => {
481
+ const dataset = new Dataset({ tiles: ['https://example.com/{z}/{x}/{y}'], sourceLayer: 'my_layer' })
482
+ expect(dataset.sourceLayer).toBe('my_layer')
483
+ })
484
+
485
+ it('returns undefined for a geojson dataset with no tiles', () => {
486
+ const dataset = new Dataset({ geojson: { type: 'FeatureCollection', features: [] } })
487
+ expect(dataset.sourceLayer).toBeUndefined()
488
+ })
489
+
490
+ it("returns the parent's sourceLayer for a sublayer", () => {
491
+ const parentDef = { id: 'parent', tiles: ['https://example.com/{z}/{x}/{y}'], sourceLayer: 'parent_layer', style: {} }
492
+ const childDef = { id: 'child', parentId: 'parent', style: {} }
493
+ datasetRegistry.attach({ parent: parentDef, child: childDef })
494
+ expect(new Dataset(childDef).sourceLayer).toBe('parent_layer')
495
+ })
496
+ })
497
+ })
@@ -0,0 +1,35 @@
1
+ export class DatasetDefinitionCache {
2
+ constructor () {
3
+ this.idToDefinitionMap = new Map()
4
+ this.definitionToInstanceMap = new Map()
5
+ }
6
+
7
+ add (registryDataset) {
8
+ this.idToDefinitionMap.set(registryDataset.id, registryDataset._datasetDefinition)
9
+ this.definitionToInstanceMap.set(registryDataset._datasetDefinition, registryDataset)
10
+ }
11
+
12
+ invalidateDataset (registryDataset) {
13
+ const existingDefinition = this.idToDefinitionMap.get(registryDataset.id)
14
+ const allIds = [existingDefinition.id, ...(existingDefinition?.sublayerIds || [])]
15
+ allIds.forEach(id => {
16
+ const existingDefinition = this.idToDefinitionMap.get(id)
17
+ this.definitionToInstanceMap.delete(existingDefinition)
18
+ this.idToDefinitionMap.delete(id)
19
+ })
20
+ }
21
+
22
+ invalidateChangedDatasets (newDatasets) {
23
+ newDatasets.forEach((datasetDefinition) => {
24
+ const existingDefinition = this.idToDefinitionMap.get(datasetDefinition.id)
25
+ if (existingDefinition && existingDefinition !== datasetDefinition) {
26
+ const instance = this.definitionToInstanceMap.get(existingDefinition)
27
+ this.invalidateDataset(instance.isSublayer && instance.parent ? instance.parent : instance)
28
+ }
29
+ })
30
+ }
31
+
32
+ getByDefinition (datasetDefinition) {
33
+ return this.definitionToInstanceMap.get(datasetDefinition)
34
+ }
35
+ }
@@ -0,0 +1,116 @@
1
+ import { createDataset } from './createDataset.js'
2
+ import { DatasetDefinitionCache } from './datasetDefinitionCache.js'
3
+
4
+ const datasetRegistry = {
5
+ attach (datasetsRef, orderedDatasetsRef) {
6
+ this._datasets = datasetsRef
7
+ this._orderedDatasets = orderedDatasetsRef
8
+ this._invalidateChangedDatasets()
9
+ },
10
+ _definitionCache: new DatasetDefinitionCache(),
11
+ _invalidateCache () { // used in tests to clear the cache between runs
12
+ this._definitionCache = new DatasetDefinitionCache()
13
+ },
14
+ _invalidateChangedDatasets () { // used in tests to clear the cache between runs
15
+ if (this._datasets) {
16
+ this._definitionCache.invalidateChangedDatasets(Object.values(this._datasets))
17
+ } else {
18
+ this._invalidateCache()
19
+ }
20
+ },
21
+ // createDataset defaults to a generic dataset factory function, but can be overridden by calling
22
+ // attachCreateDataset, which allows the layer adapter to provide its own createDataset function,
23
+ attachCreateDataset (createDataset) { this._createDataset = createDataset },
24
+ _createDataset: (datasetDefinition) => createDataset(datasetDefinition),
25
+
26
+ // getDataset retrieves a dataset by id, creating a new Dataset instance that wraps the definition
27
+ getDataset (id) {
28
+ const definition = this.datasets[id]
29
+ if (!definition) {
30
+ return undefined
31
+ }
32
+ const cachedDefinition = this._definitionCache.getByDefinition(definition)
33
+ if (cachedDefinition) {
34
+ return cachedDefinition
35
+ }
36
+ const registryDataset = this._createDataset(definition)
37
+ this._definitionCache.add(registryDataset)
38
+ if (registryDataset.isSublayer) {
39
+ // Ensure the parent dataset is also cached
40
+ this.getDataset(registryDataset.parentId)
41
+ }
42
+ return registryDataset
43
+ },
44
+
45
+ forEach (callback) {
46
+ this._orderedDatasets.forEach((datasetId) => callback(this.getDataset(datasetId)))
47
+ },
48
+
49
+ forEachDataset (callback) {
50
+ Object.values(this.datasets)
51
+ .filter(def => !def.parentId) // Only top-level datasets
52
+ .forEach((dataset) => callback(this.getDataset(dataset.id)))
53
+ },
54
+
55
+ topLevelDatasets () {
56
+ return Object.values(this.datasets)
57
+ .filter(def => !def.parentId)
58
+ .map(def => this.getDataset(def.id))
59
+ },
60
+
61
+ getPatternAndSymbolConfigs () {
62
+ return this.topLevelDatasets().reduce((acc, dataset) => {
63
+ return {
64
+ ...acc,
65
+ patternConfigs: [...acc.patternConfigs, ...dataset.patternConfigs],
66
+ symbolConfigs: [...acc.symbolConfigs, ...dataset.symbolConfigs]
67
+ }
68
+ }, { patternConfigs: [], symbolConfigs: [] })
69
+ },
70
+
71
+ _lastKeyItems: {},
72
+ keyItems () {
73
+ if (this.datasets === this._lastKeyItemsDatasets) {
74
+ return this._lastKeyItems
75
+ }
76
+ this._lastKeyItemsDatasets = this.datasets
77
+ const items = []
78
+ const seenGroups = new Set()
79
+ let hasGroups = false
80
+ this.forEachDataset((dataset) => {
81
+ if (!(dataset.showInKey && dataset.visible)) {
82
+ return
83
+ }
84
+ if (dataset.hasSublayers) {
85
+ const sublayers = dataset.sublayers.filter(sublayer => sublayer.visible)
86
+ if (sublayers.length) {
87
+ hasGroups = true
88
+ items.push({ type: 'sublayers', dataset, sublayers })
89
+ }
90
+ } else if (dataset.groupLabel) {
91
+ if (seenGroups.has(dataset.groupLabel)) {
92
+ return
93
+ }
94
+ seenGroups.add(dataset.groupLabel)
95
+ hasGroups = true
96
+ items.push({
97
+ type: 'group',
98
+ groupLabel: dataset.groupLabel,
99
+ datasets: this.topLevelDatasets()
100
+ .filter(groupDataset => (groupDataset.groupLabel === dataset.groupLabel && !groupDataset.hasSublayers && groupDataset.showInKey))
101
+ })
102
+ } else {
103
+ items.push({ type: 'flat', dataset })
104
+ }
105
+ })
106
+ this._lastKeyItems = { items, hasGroups }
107
+ return this._lastKeyItems
108
+ }
109
+ }
110
+
111
+ Object.defineProperty(datasetRegistry, 'datasets', { get: () => datasetRegistry._datasets })
112
+
113
+ // TODO remove this global reference once development is finished
114
+ window.datasetRegistry = datasetRegistry
115
+
116
+ export { datasetRegistry }