@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,273 @@
1
+ import { initialiseDatasets } from './initialiseDatasets.js'
2
+ import { createDynamicSource } from '../fetch/createDynamicSource.js'
3
+ import { datasetRegistry } from '../registry/datasetRegistry.js'
4
+
5
+ jest.mock('../fetch/createDynamicSource.js')
6
+ jest.mock('../registry/datasetRegistry.js', () => ({
7
+ datasetRegistry: {
8
+ attach: jest.fn(),
9
+ attachCreateDataset: jest.fn(),
10
+ forEachDataset: jest.fn(),
11
+ _invalidateCache: jest.fn()
12
+ }
13
+ }))
14
+
15
+ // ─── helpers ─────────────────────────────────────────────────────────────────
16
+
17
+ const makeAdapter = (overrides = {}) => ({
18
+ init: jest.fn().mockResolvedValue(undefined),
19
+ onMapStyleChange: jest.fn(),
20
+ onMapSizeChange: jest.fn(),
21
+ setData: jest.fn(),
22
+ destroy: jest.fn(),
23
+ createDataset: jest.fn(),
24
+ ...overrides
25
+ })
26
+
27
+ const makeEventBus = () => {
28
+ const handlers = {}
29
+ return {
30
+ on: jest.fn((event, handler) => { handlers[event] = handler }),
31
+ off: jest.fn(),
32
+ emit: jest.fn(),
33
+ _handlers: handlers
34
+ }
35
+ }
36
+
37
+ const makeArgs = (overrides = {}) => {
38
+ const eventBus = makeEventBus()
39
+ return {
40
+ adapter: makeAdapter(),
41
+ pluginConfig: { datasets: [{ id: 'roads', label: 'Roads' }] },
42
+ pluginStateRef: {},
43
+ mapStyle: { layers: [] },
44
+ mapProvider: { map: {} },
45
+ events: { MAP_SET_STYLE: 'map:setStyle', MAP_SIZE_CHANGE: 'map:sizeChange' },
46
+ dispatch: jest.fn(),
47
+ eventBus,
48
+ ...overrides
49
+ }
50
+ }
51
+
52
+ beforeEach(() => {
53
+ jest.clearAllMocks()
54
+ datasetRegistry.forEachDataset.mockImplementation(() => {})
55
+ })
56
+
57
+ // ─── initialisation ──────────────────────────────────────────────────────────
58
+
59
+ describe('initialiseDatasets', () => {
60
+ it('calls adapter.init with mapStyle', () => {
61
+ const args = makeArgs()
62
+ initialiseDatasets(args)
63
+ expect(args.adapter.init).toHaveBeenCalledWith(args.mapStyle)
64
+ })
65
+
66
+ it('attaches mappedDatasets to datasetRegistry', () => {
67
+ const args = makeArgs()
68
+ initialiseDatasets(args)
69
+ expect(datasetRegistry.attach).toHaveBeenCalled()
70
+ })
71
+
72
+ it('attaches adapter.createDataset to datasetRegistry when provided', () => {
73
+ const args = makeArgs()
74
+ initialiseDatasets(args)
75
+ expect(datasetRegistry.attachCreateDataset).toHaveBeenCalledWith(args.adapter.createDataset)
76
+ })
77
+
78
+ it('does not call attachCreateDataset when adapter has no createDataset', () => {
79
+ const args = makeArgs({ adapter: makeAdapter({ createDataset: undefined }) })
80
+ initialiseDatasets(args)
81
+ expect(datasetRegistry.attachCreateDataset).not.toHaveBeenCalled()
82
+ })
83
+
84
+ it('dispatches SET_GLOBAL_STATE when pluginConfig.globals is provided', () => {
85
+ const globals = { opacity: 0.8 }
86
+ const args = makeArgs({ pluginConfig: { datasets: [], globals } })
87
+ initialiseDatasets(args)
88
+ expect(args.dispatch).toHaveBeenCalledWith({ type: 'SET_GLOBAL_STATE', payload: globals })
89
+ })
90
+
91
+ it('does not dispatch SET_GLOBAL_STATE when pluginConfig.globals is absent', () => {
92
+ const args = makeArgs()
93
+ initialiseDatasets(args)
94
+ expect(args.dispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_GLOBAL_STATE' }))
95
+ })
96
+
97
+ it('registers MAP_SET_STYLE and MAP_SIZE_CHANGE event listeners', () => {
98
+ const args = makeArgs()
99
+ initialiseDatasets(args)
100
+ expect(args.eventBus.on).toHaveBeenCalledWith('map:setStyle', expect.any(Function))
101
+ expect(args.eventBus.on).toHaveBeenCalledWith('map:sizeChange', expect.any(Function))
102
+ })
103
+
104
+ describe('after adapter.init resolves', () => {
105
+ it('dispatches SET_DATASETS', async () => {
106
+ const args = makeArgs()
107
+ initialiseDatasets(args)
108
+ await Promise.resolve() // flush microtask
109
+ expect(args.dispatch).toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_DATASETS' }))
110
+ })
111
+
112
+ it('emits datasets:ready', async () => {
113
+ const args = makeArgs()
114
+ initialiseDatasets(args)
115
+ await Promise.resolve()
116
+ expect(args.eventBus.emit).toHaveBeenCalledWith('datasets:ready')
117
+ })
118
+
119
+ it('creates dynamic sources for datasets with dynamicGeoJSON', async () => {
120
+ const mockDynamicSource = { destroy: jest.fn(), refresh: jest.fn(), clear: jest.fn(), getFeatureCount: jest.fn() }
121
+ createDynamicSource.mockReturnValue(mockDynamicSource)
122
+
123
+ const dynamicDataset = {
124
+ id: 'parcels',
125
+ hasDynamicGeoJSON: true,
126
+ dynamicGeoJSON: { url: 'http://example.com/geojson', idProperty: 'id' }
127
+ }
128
+ datasetRegistry.forEachDataset.mockImplementation(cb => cb(dynamicDataset))
129
+
130
+ const args = makeArgs()
131
+ initialiseDatasets(args)
132
+ await Promise.resolve()
133
+
134
+ expect(createDynamicSource).toHaveBeenCalledWith(expect.objectContaining({
135
+ dynamicGeoJSON: dynamicDataset.dynamicGeoJSON,
136
+ map: args.mapProvider.map
137
+ }))
138
+ })
139
+
140
+ it('does not create dynamic sources for datasets without dynamicGeoJSON', async () => {
141
+ const nonDynamicDataset = { id: 'roads', hasDynamicGeoJSON: false }
142
+ datasetRegistry.forEachDataset.mockImplementation(cb => cb(nonDynamicDataset))
143
+
144
+ const args = makeArgs()
145
+ initialiseDatasets(args)
146
+ await Promise.resolve()
147
+
148
+ expect(createDynamicSource).not.toHaveBeenCalled()
149
+ })
150
+ })
151
+ })
152
+
153
+ // ─── event handlers ───────────────────────────────────────────────────────────
154
+
155
+ describe('event handlers', () => {
156
+ it('delegates MAP_SET_STYLE to adapter.onMapStyleChange', () => {
157
+ const args = makeArgs()
158
+ initialiseDatasets(args)
159
+ const newStyle = { layers: [{ id: 'new' }] }
160
+ args.eventBus._handlers['map:setStyle'](newStyle)
161
+ expect(args.adapter.onMapStyleChange).toHaveBeenCalledWith(newStyle, expect.any(Map))
162
+ })
163
+
164
+ it('delegates MAP_SIZE_CHANGE to adapter.onMapSizeChange with current mapStyle', () => {
165
+ const args = makeArgs()
166
+ initialiseDatasets(args)
167
+ args.eventBus._handlers['map:sizeChange']()
168
+ expect(args.adapter.onMapSizeChange).toHaveBeenCalledWith(args.mapStyle)
169
+ })
170
+
171
+ it('uses the updated mapStyle after MAP_SET_STYLE', () => {
172
+ const args = makeArgs()
173
+ initialiseDatasets(args)
174
+ const newStyle = { layers: [{ id: 'updated' }] }
175
+ args.eventBus._handlers['map:setStyle'](newStyle)
176
+ args.eventBus._handlers['map:sizeChange']()
177
+ expect(args.adapter.onMapSizeChange).toHaveBeenCalledWith(newStyle)
178
+ })
179
+ })
180
+
181
+ // ─── returned API ─────────────────────────────────────────────────────────────
182
+
183
+ describe('returned API', () => {
184
+ it('remove() unregisters event listeners and destroys adapter', () => {
185
+ const args = makeArgs()
186
+ const instance = initialiseDatasets(args)
187
+ instance.remove()
188
+ expect(args.eventBus.off).toHaveBeenCalledWith('map:setStyle', expect.any(Function))
189
+ expect(args.eventBus.off).toHaveBeenCalledWith('map:sizeChange', expect.any(Function))
190
+ expect(args.adapter.destroy).toHaveBeenCalled()
191
+ })
192
+
193
+ it('remove() destroys all dynamic sources', async () => {
194
+ const mockSource = { destroy: jest.fn(), refresh: jest.fn(), clear: jest.fn(), getFeatureCount: jest.fn() }
195
+ createDynamicSource.mockReturnValue(mockSource)
196
+ const mockDataset = { id: 'parcels', hasDynamicGeoJSON: true, dynamicGeoJSON: { url: 'http://x.com', idProperty: 'id' } }
197
+ datasetRegistry.forEachDataset.mockImplementation(cb =>
198
+ cb(mockDataset)
199
+ )
200
+
201
+ const args = makeArgs()
202
+ const instance = initialiseDatasets(args)
203
+ await Promise.resolve()
204
+ instance.remove()
205
+
206
+ expect(mockSource.destroy).toHaveBeenCalled()
207
+ })
208
+
209
+ it('refreshDataset() calls refresh on the matching dynamic source', async () => {
210
+ const mockSource = { destroy: jest.fn(), refresh: jest.fn(), clear: jest.fn(), getFeatureCount: jest.fn() }
211
+ createDynamicSource.mockReturnValue(mockSource)
212
+ const mockDataset = { id: 'parcels', hasDynamicGeoJSON: true, dynamicGeoJSON: { url: 'http://x.com', idProperty: 'id' } }
213
+ datasetRegistry.forEachDataset.mockImplementation(cb =>
214
+ cb(mockDataset)
215
+ )
216
+
217
+ const args = makeArgs()
218
+ const instance = initialiseDatasets(args)
219
+ await Promise.resolve()
220
+ instance.refreshDataset('parcels')
221
+
222
+ expect(mockSource.refresh).toHaveBeenCalled()
223
+ })
224
+
225
+ it('clearDatasetCache() calls clear on the matching dynamic source', async () => {
226
+ const mockSource = { destroy: jest.fn(), refresh: jest.fn(), clear: jest.fn(), getFeatureCount: jest.fn() }
227
+ createDynamicSource.mockReturnValue(mockSource)
228
+ const mockDataset = { id: 'parcels', hasDynamicGeoJSON: true, dynamicGeoJSON: { url: 'http://x.com', idProperty: 'id' } }
229
+ datasetRegistry.forEachDataset.mockImplementation(cb =>
230
+ cb(mockDataset)
231
+ )
232
+
233
+ const args = makeArgs()
234
+ const instance = initialiseDatasets(args)
235
+ await Promise.resolve()
236
+ instance.clearDatasetCache('parcels')
237
+
238
+ expect(mockSource.clear).toHaveBeenCalled()
239
+ })
240
+
241
+ it('getFeatureCount() returns the count from the matching dynamic source', async () => {
242
+ const mockSource = { destroy: jest.fn(), refresh: jest.fn(), clear: jest.fn(), getFeatureCount: jest.fn().mockReturnValue(42) }
243
+ createDynamicSource.mockReturnValue(mockSource)
244
+ const mockDataset = { id: 'parcels', hasDynamicGeoJSON: true, dynamicGeoJSON: { url: 'http://x.com', idProperty: 'id' } }
245
+ datasetRegistry.forEachDataset.mockImplementation(cb =>
246
+ cb(mockDataset)
247
+ )
248
+
249
+ const args = makeArgs()
250
+ const instance = initialiseDatasets(args)
251
+ await Promise.resolve()
252
+
253
+ expect(instance.getFeatureCount('parcels')).toBe(42)
254
+ })
255
+
256
+ it('getFeatureCount() returns null for a non-dynamic dataset', () => {
257
+ const args = makeArgs()
258
+ const instance = initialiseDatasets(args)
259
+ expect(instance.getFeatureCount('unknown')).toBeNull()
260
+ })
261
+
262
+ it('refreshDataset() does nothing for a non-dynamic dataset', () => {
263
+ const args = makeArgs()
264
+ const instance = initialiseDatasets(args)
265
+ expect(() => instance.refreshDataset('unknown')).not.toThrow()
266
+ })
267
+
268
+ it('clearDatasetCache() does nothing for a non-dynamic dataset', () => {
269
+ const args = makeArgs()
270
+ const instance = initialiseDatasets(args)
271
+ expect(() => instance.clearDatasetCache('unknown')).not.toThrow()
272
+ })
273
+ })
@@ -1,8 +1,8 @@
1
1
  // /plugins/datasets/manifest.js
2
- import { initialState, actions } from './reducer.js'
3
- import { DatasetsInit } from './DatasetsInit.jsx'
4
- import { Layers } from './panels/Layers.jsx'
5
- import { Key } from './panels/Key.jsx'
2
+ import { initialState, actions } from './reducers/pluginState.js'
3
+ import { DatasetsInit } from './initialise/DatasetsInit.jsx'
4
+ import { LayersMenu } from './components/LayersMenu/LayersMenu.jsx'
5
+ import { Key } from './components/Key/Key.jsx'
6
6
  import { addDataset } from './api/addDataset.js'
7
7
  import { removeDataset } from './api/removeDataset.js'
8
8
  import { setDatasetVisibility } from './api/setDatasetVisibility.js'
@@ -12,6 +12,7 @@ import { getStyle } from './api/getStyle.js'
12
12
  import { setOpacity } from './api/setOpacity.js'
13
13
  import { getOpacity } from './api/getOpacity.js'
14
14
  import { setData } from './api/setData.js'
15
+ import { setGlobals } from './api/setGlobals.js'
15
16
 
16
17
  export const manifest = {
17
18
  InitComponent: DatasetsInit,
@@ -42,7 +43,7 @@ export const manifest = {
42
43
  exclusive: true,
43
44
  width: '280px'
44
45
  },
45
- render: Layers
46
+ render: LayersMenu
46
47
  }, {
47
48
  id: 'datasetsKey',
48
49
  label: 'Key',
@@ -118,6 +119,7 @@ export const manifest = {
118
119
  getStyle,
119
120
  setOpacity,
120
121
  getOpacity,
121
- setData
122
+ setData,
123
+ setGlobals
122
124
  }
123
125
  }
@@ -0,0 +1,253 @@
1
+ const pointData = {
2
+ type: 'FeatureCollection',
3
+ features: [{
4
+ type: 'Feature',
5
+ properties: { category: 'prehistoric' },
6
+ geometry: { coordinates: [-2.4558622, 54.5617135], type: 'Point' }
7
+ }, {
8
+ type: 'Feature',
9
+ properties: { category: 'roman' },
10
+ geometry: { coordinates: [-2.439823, 54.5525437], type: 'Point' }
11
+ },
12
+ {
13
+ type: 'Feature',
14
+ properties: { category: 'medieval' },
15
+ geometry: { coordinates: [-2.4481939, 54.5575261], type: 'Point' }
16
+ }]
17
+ }
18
+ export const datasets = [
19
+ {
20
+ id: 'land-covers',
21
+ label: 'Land covers',
22
+ dynamicGeoJSON: {
23
+ idProperty: 'id', // required - the ID that identifies individual features
24
+ url: `${process.env.FARMING_API_URL}/api/collections/parcels/items?sbi=106325052`, // required
25
+ transformRequest: (url) => url + 'TRANSFORMED', // Required
26
+ maxFeatures: 50000 // Optional: evict distant features when exceeded
27
+ },
28
+ hiddenFeatures: [42],
29
+ query: {},
30
+ maxFeatures: 50000, // Optional: evict distant features when exceeded
31
+ minZoom: 10,
32
+ maxZoom: 24,
33
+ showInKey: true,
34
+ showInMenu: true,
35
+ visible: true,
36
+ sublayers: [{
37
+ id: '130-131',
38
+ label: 'Permanent grassland',
39
+ filter: ['in', ['get', 'dominant_land_cover'], ['literal', ['130', '131']]], // 'dominant_land_cover = "130"'
40
+ showInMenu: true,
41
+ visible: true,
42
+ style: {
43
+ stroke: { outdoor: '#00897B', dark: '#ffffff' },
44
+ fillPattern: 'diagonal-cross-hatch',
45
+ fillPatternForegroundColor: { outdoor: '#00897B', dark: '#ffffff' },
46
+ fillPatternBackgroundColor: 'transparent'
47
+ }
48
+ }, {
49
+ id: '332',
50
+ label: 'Woodland',
51
+ filter: ['==', ['get', 'dominant_land_cover'], '332'],
52
+ showInMenu: true,
53
+ visible: true,
54
+ style: {
55
+ stroke: { outdoor: '#2E7D32', dark: '#ffffff' },
56
+ fillPattern: 'dot',
57
+ fillPatternForegroundColor: { outdoor: '#2E7D32', dark: '#ffffff' },
58
+ fillPatternBackgroundColor: 'transparent'
59
+ }
60
+ }, {
61
+ id: '110',
62
+ label: 'Arable',
63
+ filter: ['==', ['get', 'dominant_land_cover'], '110'],
64
+ showInMenu: true,
65
+ visible: true,
66
+ style: {
67
+ stroke: { outdoor: '#6D4C41', dark: '#ffffff' },
68
+ fillPattern: 'horizontal-hatch',
69
+ fillPatternForegroundColor: { outdoor: '#6D4C41', dark: '#ffffff' },
70
+ fillPatternBackgroundColor: 'transparent'
71
+ }
72
+ }, {
73
+ id: '379',
74
+ label: 'Farmyards',
75
+ visibility: 'hidden',
76
+ visible: false,
77
+ filter: ['==', ['get', 'dominant_land_cover'], '379'],
78
+ showInMenu: true,
79
+ style: {
80
+ stroke: { outdoor: '#6A1B9A', dark: '#ffffff' },
81
+ fillPattern: 'forward-diagonal-hatch',
82
+ fillPatternForegroundColor: { outdoor: '#6A1B9A', dark: '#ffffff' },
83
+ fillPatternBackgroundColor: 'transparent'
84
+ }
85
+ }, {
86
+ id: 'other',
87
+ label: 'Others',
88
+ filter: ['!', ['in', ['get', 'dominant_land_cover'], ['literal', ['110', '130', '131', '332', '379']]]],
89
+ showInMenu: true,
90
+ visible: true,
91
+ style: {
92
+ stroke: { outdoor: '#1565C0', dark: '#ffffff' },
93
+ fill: 'rgba(0,0,255,0.1)',
94
+ fillPattern: 'vertical-hatch',
95
+ fillPatternForegroundColor: { outdoor: '#1565C0', dark: '#ffffff' }
96
+ // fillPatternBackgroundColor: 'transparent'
97
+ }
98
+ }]
99
+ },
100
+ {
101
+ id: 'existing-fields',
102
+ label: 'Existing fields',
103
+ // groupLabel: 'Test group',
104
+ filter: ['all', ['==', ['get', 'sbi'], '106223377'], ['==', ['get', 'is_dominant_land_cover'], true]],
105
+ tiles: 'https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}',
106
+ sourceLayer: 'field_parcels_filtered',
107
+ minZoom: 10,
108
+ maxZoom: 24,
109
+ showInKey: true,
110
+ showInMenu: true,
111
+ visible: true,
112
+ style: {
113
+ stroke: { outdoor: '#1565C0', dark: '#ffffff' },
114
+ strokeWidth: 2,
115
+ fill: 'rgba(21,101,192,0.1)',
116
+ symbolDescription: { outdoor: 'blue outline', dark: 'white outline' }
117
+ }
118
+ }, {
119
+ id: 'historic-monuments',
120
+ label: 'Historic monuments',
121
+ geojson: pointData,
122
+ minZoom: 10,
123
+ maxZoom: 24,
124
+ showInKey: true,
125
+ showInMenu: true,
126
+ visible: true,
127
+ style: {
128
+ symbol: 'square',
129
+ symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z' // Historic monument
130
+ },
131
+ sublayers: [{
132
+ id: 'prehistoric',
133
+ label: 'Prehistoric',
134
+ filter: ['in', ['get', 'category'], 'prehistoric'],
135
+ showInMenu: true,
136
+ visible: true,
137
+ style: {
138
+ symbolBackgroundColor: '#00897B'
139
+ }
140
+ }, {
141
+ id: 'roman',
142
+ label: 'Roman',
143
+ filter: ['in', ['get', 'category'], 'roman'],
144
+ showInMenu: true,
145
+ visible: true,
146
+ style: {
147
+ symbolBackgroundColor: '#ca3535'
148
+ }
149
+ }, {
150
+ id: 'medieval',
151
+ label: 'Medieval',
152
+ filter: ['in', ['get', 'category'], 'medieval'],
153
+ showInMenu: true,
154
+ visible: true,
155
+ style: {
156
+ symbolBackgroundColor: '#1565C0'
157
+ }
158
+ }]
159
+ }, {
160
+ id: 'hedge-control',
161
+ label: 'Hedge control',
162
+ // groupLabel: 'Test group',
163
+ tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'],
164
+ sourceLayer: 'hedge_control',
165
+ minZoom: 10,
166
+ maxZoom: 24,
167
+ showInKey: true,
168
+ showInMenu: true,
169
+ visibility: 'hidden',
170
+ visible: false,
171
+ style: {
172
+ stroke: '#b58840',
173
+ fill: 'transparent',
174
+ strokeWidth: 4,
175
+ symbolDescription: { outdoor: 'blue outline' },
176
+ keySymbolShape: 'line'
177
+ }
178
+ }
179
+ ]
180
+
181
+ export const expectedDatasetsMenuConfig = [
182
+ {
183
+ id: 'land-covers',
184
+ groupLabel: 'Land covers',
185
+ visibleWhen: true,
186
+ type: 'checkbox',
187
+ items: [
188
+ { id: 'land-covers-130-131', label: 'Permanent grassland' },
189
+ { id: 'land-covers-332', label: 'Woodland' },
190
+ { id: 'land-covers-110', label: 'Arable' },
191
+ { id: 'land-covers-379', label: 'Farmyards' },
192
+ { id: 'land-covers-other', label: 'Others' }
193
+ ]
194
+ },
195
+ {
196
+ visibleWhen: true,
197
+ type: 'checkbox',
198
+ id: 'existing-fields',
199
+ items: [
200
+ { id: 'existing-fields', label: 'Existing fields' }
201
+ ]
202
+ },
203
+ {
204
+ id: 'historic-monuments',
205
+ groupLabel: 'Historic monuments',
206
+ visibleWhen: true,
207
+ type: 'checkbox',
208
+ items: [
209
+ { id: 'historic-monuments-prehistoric', label: 'Prehistoric' },
210
+ { id: 'historic-monuments-roman', label: 'Roman' },
211
+ { id: 'historic-monuments-medieval', label: 'Medieval' }
212
+ ]
213
+ },
214
+ {
215
+ visibleWhen: true,
216
+ type: 'checkbox',
217
+ id: 'hedge-control',
218
+ items: [
219
+ { id: 'hedge-control', label: 'Hedge control' }
220
+ ]
221
+ }
222
+ ]
223
+
224
+ const landCovers = datasets[0]
225
+ const existingFields = datasets[1]
226
+ const historicMonuments = datasets[2]
227
+ const hedgeControl = datasets[3]
228
+ const landCoversMenuItem = expectedDatasetsMenuConfig[0]
229
+ const existingFieldsMenuItem = expectedDatasetsMenuConfig[1]
230
+ const historicMonumentsMenuItem = expectedDatasetsMenuConfig[2]
231
+ const hedgeControlMenuItem = expectedDatasetsMenuConfig[3]
232
+
233
+ export const datasetsWithGroups = [
234
+ { ...landCovers },
235
+ { ...existingFields, groupLabel: 'Test group' },
236
+ { ...historicMonuments },
237
+ { ...hedgeControl, groupLabel: 'Test group', visible: true }
238
+ ]
239
+
240
+ export const expectedDatasetsMenuConfigWithGroups = [
241
+ { ...landCoversMenuItem },
242
+ {
243
+ visibleWhen: true,
244
+ type: 'checkbox',
245
+ groupLabel: 'Test group',
246
+ id: 'Test group',
247
+ items: [
248
+ ...existingFieldsMenuItem.items,
249
+ ...hedgeControlMenuItem.items
250
+ ]
251
+ },
252
+ { ...historicMonumentsMenuItem }
253
+ ]
@@ -0,0 +1,83 @@
1
+ export const datasetsToMenu = (state) => {
2
+ const { datasets = [] } = state
3
+ const menu = []
4
+ const datasetsForMenu = datasets.filter(dataset =>
5
+ dataset.showInMenu || dataset.sublayers?.some(s => s.showInMenu)
6
+ )
7
+ const groups = new Map()
8
+ datasetsForMenu.forEach(dataset => {
9
+ if (dataset.sublayers?.length) {
10
+ const visibleSublayers = dataset.sublayers.filter(sublayer =>
11
+ dataset.showInMenu ? sublayer.showInMenu !== false : sublayer.showInMenu
12
+ )
13
+ if (!visibleSublayers.length) { return }
14
+ menu.push({
15
+ id: dataset.id,
16
+ groupLabel: dataset.label,
17
+ visibleWhen: true,
18
+ type: 'checkbox',
19
+ items: visibleSublayers.map(sublayer => ({
20
+ id: `${dataset.id}-${sublayer.id}`,
21
+ label: sublayer.label
22
+ }))
23
+ })
24
+ } else if (dataset.groupLabel) {
25
+ // Check for existing group object for this groupLabel, or create it if it doesn't exist,
26
+ // then add the dataset to its items and push it to the menu if it's new
27
+ const groupObject = groups.has(dataset.groupLabel)
28
+ ? groups.get(dataset.groupLabel)
29
+ : {
30
+ id: dataset.groupLabel,
31
+ groupLabel: dataset.groupLabel,
32
+ visibleWhen: true,
33
+ type: 'checkbox',
34
+ items: []
35
+ }
36
+ if (!groups.has(dataset.groupLabel)) {
37
+ groups.set(dataset.groupLabel, groupObject)
38
+ menu.push(groupObject)
39
+ }
40
+ // Then add this dataset to the groupObject's items
41
+ groupObject.items.push({
42
+ id: dataset.id,
43
+ label: dataset.label
44
+ })
45
+ } else {
46
+ menu.push({
47
+ visibleWhen: true,
48
+ type: 'checkbox',
49
+ id: dataset.id,
50
+ items: [
51
+ {
52
+ id: dataset.id,
53
+ label: dataset.label
54
+ }
55
+ ]
56
+ })
57
+ }
58
+ })
59
+ return menu
60
+ }
61
+
62
+ export const addDatasetToMenu = (state, dataset) => {
63
+ const menu = [...state.menu]
64
+ const datasetMenuEntry = datasetsToMenu({ datasets: [dataset] })
65
+ // For each entry in the new dataset menu, either add it to an existing group or add it as a new entry
66
+ const existingGroup = menu.find(entry => entry.groupLabel === datasetMenuEntry[0].groupLabel)
67
+ if (existingGroup) {
68
+ existingGroup.items.push(...datasetMenuEntry[0].items)
69
+ } else {
70
+ menu.push(...datasetMenuEntry)
71
+ }
72
+ return menu
73
+ }
74
+
75
+ export const removeDatasetsFromMenu = (menu, datasetsToRemove) => {
76
+ return menu.reduce((newMenu, menuGroup) => {
77
+ const filteredItems = menuGroup.items.filter(item => !datasetsToRemove.includes(item.id))
78
+ if (filteredItems.length) {
79
+ newMenu.push({ ...menuGroup, items: filteredItems })
80
+ }
81
+ return newMenu
82
+ }, [])
83
+ }