@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,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,78 @@
1
+ export const datasetsToMenu = (state) => {
2
+ const { datasets = [] } = state
3
+ const menu = []
4
+ const datasetsForMenu = datasets.filter(dataset => dataset.showInMenu)
5
+ const groups = new Map()
6
+ datasetsForMenu.forEach(dataset => {
7
+ if (dataset.sublayers?.length) {
8
+ menu.push({
9
+ id: dataset.id,
10
+ groupLabel: dataset.label,
11
+ visibleWhen: true,
12
+ type: 'checkbox',
13
+ items: dataset.sublayers.filter(sublayer => sublayer.showInMenu)
14
+ .map(sublayer => ({
15
+ id: `${dataset.id}-${sublayer.id}`,
16
+ label: sublayer.label
17
+ }))
18
+ })
19
+ } else if (dataset.groupLabel) {
20
+ // Check for existing group object for this groupLabel, or create it if it doesn't exist,
21
+ // then add the dataset to its items and push it to the menu if it's new
22
+ const groupObject = groups.has(dataset.groupLabel)
23
+ ? groups.get(dataset.groupLabel)
24
+ : {
25
+ id: dataset.groupLabel,
26
+ groupLabel: dataset.groupLabel,
27
+ visibleWhen: true,
28
+ type: 'checkbox',
29
+ items: []
30
+ }
31
+ if (!groups.has(dataset.groupLabel)) {
32
+ groups.set(dataset.groupLabel, groupObject)
33
+ menu.push(groupObject)
34
+ }
35
+ // Then add this dataset to the groupObject's items
36
+ groupObject.items.push({
37
+ id: dataset.id,
38
+ label: dataset.label
39
+ })
40
+ } else {
41
+ menu.push({
42
+ visibleWhen: true,
43
+ type: 'checkbox',
44
+ id: dataset.id,
45
+ items: [
46
+ {
47
+ id: dataset.id,
48
+ label: dataset.label
49
+ }
50
+ ]
51
+ })
52
+ }
53
+ })
54
+ return menu
55
+ }
56
+
57
+ export const addDatasetToMenu = (state, dataset) => {
58
+ const menu = [...state.menu]
59
+ const datasetMenuEntry = datasetsToMenu({ datasets: [dataset] })
60
+ // For each entry in the new dataset menu, either add it to an existing group or add it as a new entry
61
+ const existingGroup = menu.find(entry => entry.groupLabel === datasetMenuEntry[0].groupLabel)
62
+ if (existingGroup) {
63
+ existingGroup.items.push(...datasetMenuEntry[0].items)
64
+ } else {
65
+ menu.push(...datasetMenuEntry)
66
+ }
67
+ return menu
68
+ }
69
+
70
+ export const removeDatasetsFromMenu = (menu, datasetsToRemove) => {
71
+ return menu.reduce((newMenu, menuGroup) => {
72
+ const filteredItems = menuGroup.items.filter(item => !datasetsToRemove.includes(item.id))
73
+ if (filteredItems.length) {
74
+ newMenu.push({ ...menuGroup, items: filteredItems })
75
+ }
76
+ return newMenu
77
+ }, [])
78
+ }
@@ -0,0 +1,35 @@
1
+ import {
2
+ datasets, expectedDatasetsMenuConfig,
3
+ datasetsWithGroups, expectedDatasetsMenuConfigWithGroups
4
+ } from './__data__/demoDatasets.js'
5
+
6
+ import { datasetsToMenu } from './datasetsToMenu'
7
+
8
+ describe('datasetsToMenu', () => {
9
+ describe('Missing or empty datasets', () => {
10
+ it('handles empty datasets', async () => {
11
+ expect(datasetsToMenu({ datasets: [] }))
12
+ .toEqual([])
13
+ })
14
+
15
+ it('handles missing datasets', async () => {
16
+ expect(datasetsToMenu({}))
17
+ .toEqual([])
18
+ })
19
+ })
20
+
21
+ describe('Datasets without groups', () => {
22
+ it('transforms datasets to menu config', async () => {
23
+ expect(datasetsToMenu({ datasets }))
24
+ .toEqual(expectedDatasetsMenuConfig)
25
+ })
26
+ })
27
+
28
+ describe('Datasets with groups', () => {
29
+ it('transforms datasets with groups to menu config', async () => {
30
+ const datasetsWithGroupsMenuConfig = datasetsToMenu({ datasets: datasetsWithGroups })
31
+ expect(datasetsWithGroupsMenuConfig)
32
+ .toEqual(expectedDatasetsMenuConfigWithGroups)
33
+ })
34
+ })
35
+ })