@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,386 @@
1
+ import { initialState, actions } from './pluginState.js'
2
+ import { logger } from '../../../../../src/services/logger.js'
3
+
4
+ jest.mock('../../../../../src/services/logger.js', () => ({
5
+ logger: { error: jest.fn() }
6
+ }))
7
+
8
+ // Helper: build a minimal plugin state with some mapped datasets
9
+ const makeState = (overrides = {}) => ({
10
+ ...initialState,
11
+ mappedDatasets: {},
12
+ orderedDatasets: [],
13
+ menu: [],
14
+ ...overrides
15
+ })
16
+
17
+ // Helper: build a state that already contains a dataset
18
+ const makeStateWithDataset = (id = 'roads', extraDatasetProps = {}) => ({
19
+ ...makeState(),
20
+ mappedDatasets: {
21
+ [id]: { id, label: 'Roads', visible: true, style: {}, ...extraDatasetProps }
22
+ },
23
+ orderedDatasets: [id],
24
+ menu: [{ label: 'Default', items: [{ id, label: 'Roads', type: 'item' }] }]
25
+ })
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks()
29
+ })
30
+
31
+ // ─── initialState ────────────────────────────────────────────────────────────
32
+
33
+ describe('initialState', () => {
34
+ it('has default globals', () => {
35
+ expect(initialState.globals).toEqual({
36
+ visible: true,
37
+ opacity: 1,
38
+ opacityMode: 'dataset'
39
+ })
40
+ })
41
+
42
+ it('has an empty key', () => {
43
+ expect(initialState.key).toEqual({ items: [], hasGroups: false })
44
+ })
45
+
46
+ it('has an empty actionsArray', () => {
47
+ expect(initialState.actionsArray).toEqual([])
48
+ })
49
+ })
50
+
51
+ // ─── SET_DATASETS ─────────────────────────────────────────────────────────────
52
+
53
+ describe('SET_DATASETS', () => {
54
+ it('sets mappedDatasets, orderedDatasets and menu from payload', () => {
55
+ const state = makeState()
56
+ const payload = {
57
+ datasets: [{ id: 'parks', label: 'Parks', showInMenu: true }],
58
+ mappedDatasets: { parks: { id: 'parks', label: 'Parks' } },
59
+ orderedDatasets: ['parks'],
60
+ menu: [{ id: 'parks', label: 'Parks', type: 'item' }]
61
+ }
62
+ const result = actions.SET_DATASETS(state, payload)
63
+ expect(result.mappedDatasets).toEqual(payload.mappedDatasets)
64
+ expect(result.orderedDatasets).toEqual(payload.orderedDatasets)
65
+ expect(result.menu).toEqual(payload.menu)
66
+ })
67
+
68
+ it('derives menu from datasets when menu is not in payload', () => {
69
+ const state = makeState()
70
+ const payload = {
71
+ datasets: [{ id: 'parks', label: 'Parks', showInMenu: true }],
72
+ mappedDatasets: { parks: { id: 'parks', label: 'Parks' } },
73
+ orderedDatasets: ['parks']
74
+ }
75
+ const result = actions.SET_DATASETS(state, payload)
76
+ // Menu is derived; it should be an array
77
+ expect(Array.isArray(result.menu)).toBe(true)
78
+ })
79
+
80
+ it('does not add to actionsArray', () => {
81
+ const state = makeState()
82
+ const payload = {
83
+ datasets: [],
84
+ mappedDatasets: {},
85
+ orderedDatasets: []
86
+ }
87
+ const result = actions.SET_DATASETS(state, payload)
88
+ expect(result.actionsArray).toEqual([])
89
+ })
90
+ })
91
+
92
+ // ─── ADD_DATASET ──────────────────────────────────────────────────────────────
93
+
94
+ describe('ADD_DATASET', () => {
95
+ it('adds a new dataset and queues an addDataset action', () => {
96
+ const state = makeState()
97
+ const dataset = { id: 'rivers', label: 'Rivers', showInMenu: true }
98
+ const mapStyle = { layers: [] }
99
+
100
+ const result = actions.ADD_DATASET(state, { dataset, mapStyle })
101
+
102
+ expect(result.mappedDatasets).toHaveProperty('rivers')
103
+ expect(result.orderedDatasets).toContain('rivers')
104
+ expect(result.actionsArray).toHaveLength(1)
105
+ expect(result.actionsArray[0]).toMatchObject({
106
+ method: 'addDataset',
107
+ parameters: ['rivers', mapStyle]
108
+ })
109
+ })
110
+
111
+ it('logs error and returns unchanged state when dataset already exists', () => {
112
+ const state = makeStateWithDataset('roads')
113
+ const result = actions.ADD_DATASET(state, { dataset: { id: 'roads', label: 'Roads' }, mapStyle: {} })
114
+
115
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("'roads'"))
116
+ expect(result).toBe(state)
117
+ })
118
+ })
119
+
120
+ // ─── REMOVE_DATASET ───────────────────────────────────────────────────────────
121
+
122
+ describe('REMOVE_DATASET', () => {
123
+ it('removes the dataset from mappedDatasets, orderedDatasets and menu', () => {
124
+ const state = makeStateWithDataset('roads')
125
+ const result = actions.REMOVE_DATASET(state, { id: 'roads' })
126
+
127
+ expect(result.mappedDatasets).not.toHaveProperty('roads')
128
+ expect(result.orderedDatasets).not.toContain('roads')
129
+ })
130
+
131
+ it('removes sublayers when parent is removed', () => {
132
+ const state = makeState({
133
+ mappedDatasets: {
134
+ parks: { id: 'parks', label: 'Parks', sublayerIds: ['parks-zone-a'] },
135
+ 'parks-zone-a': { id: 'parks-zone-a', parentId: 'parks' }
136
+ },
137
+ orderedDatasets: ['parks', 'parks-zone-a'],
138
+ menu: []
139
+ })
140
+ const result = actions.REMOVE_DATASET(state, { id: 'parks' })
141
+
142
+ expect(result.mappedDatasets).not.toHaveProperty('parks')
143
+ expect(result.mappedDatasets).not.toHaveProperty('parks-zone-a')
144
+ expect(result.orderedDatasets).toEqual([])
145
+ })
146
+
147
+ it('logs error and returns unchanged state when dataset does not exist', () => {
148
+ const state = makeState()
149
+ const result = actions.REMOVE_DATASET(state, { id: 'unknown' })
150
+
151
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("'unknown'"))
152
+ expect(result).toBe(state)
153
+ })
154
+ })
155
+
156
+ // ─── SET_DATASET_VISIBILITY ───────────────────────────────────────────────────
157
+
158
+ describe('SET_DATASET_VISIBILITY', () => {
159
+ it('updates dataset visibility and queues applyDatasetVisibility action', () => {
160
+ const state = makeStateWithDataset('roads')
161
+ const result = actions.SET_DATASET_VISIBILITY(state, { datasetId: 'roads', visible: false })
162
+
163
+ expect(result.mappedDatasets.roads.visible).toBe(false)
164
+ expect(result.actionsArray).toHaveLength(1)
165
+ expect(result.actionsArray[0]).toMatchObject({
166
+ method: 'applyDatasetVisibility',
167
+ parameters: ['roads', false]
168
+ })
169
+ })
170
+
171
+ it('logs error and returns unchanged state when dataset does not exist', () => {
172
+ const state = makeState()
173
+ const result = actions.SET_DATASET_VISIBILITY(state, { datasetId: 'unknown', visible: false })
174
+
175
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("'unknown'"))
176
+ expect(result).toBe(state)
177
+ })
178
+ })
179
+
180
+ // ─── SET_GLOBAL_VISIBILITY ────────────────────────────────────────────────────
181
+
182
+ describe('SET_GLOBAL_VISIBILITY', () => {
183
+ it('updates globals.visible and queues applyGlobalVisibility action', () => {
184
+ const state = makeState()
185
+ const result = actions.SET_GLOBAL_VISIBILITY(state, { visible: false })
186
+
187
+ expect(result.globals.visible).toBe(false)
188
+ expect(result.actionsArray).toHaveLength(1)
189
+ expect(result.actionsArray[0]).toMatchObject({
190
+ method: 'applyGlobalVisibility',
191
+ parameters: [false]
192
+ })
193
+ })
194
+ })
195
+
196
+ // ─── SET_DATASET_STYLE ────────────────────────────────────────────────────────
197
+
198
+ describe('SET_DATASET_STYLE', () => {
199
+ it('merges style changes and queues applyStyle action', () => {
200
+ const state = makeStateWithDataset('roads', { style: { stroke: 'red' } })
201
+ const mapStyle = { layers: [] }
202
+ const result = actions.SET_DATASET_STYLE(state, {
203
+ datasetId: 'roads',
204
+ styleChanges: { fill: 'blue' },
205
+ mapStyle
206
+ })
207
+
208
+ expect(result.mappedDatasets.roads.style).toMatchObject({ stroke: 'red', fill: 'blue' })
209
+ expect(result.actionsArray).toHaveLength(1)
210
+ expect(result.actionsArray[0]).toMatchObject({
211
+ method: 'applyStyle',
212
+ parameters: ['roads', mapStyle]
213
+ })
214
+ })
215
+
216
+ it('logs error and returns unchanged state when dataset does not exist', () => {
217
+ const state = makeState()
218
+ const result = actions.SET_DATASET_STYLE(state, { datasetId: 'unknown', styleChanges: {}, mapStyle: {} })
219
+
220
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("'unknown'"))
221
+ expect(result).toBe(state)
222
+ })
223
+ })
224
+
225
+ // ─── SET_OPACITY ──────────────────────────────────────────────────────────────
226
+
227
+ describe('SET_OPACITY', () => {
228
+ it('sets opacity in dataset style and queues applyDatasetOpacity action', () => {
229
+ const state = makeStateWithDataset('roads', { style: {} })
230
+ const result = actions.SET_OPACITY(state, { datasetId: 'roads', opacity: 0.5 })
231
+
232
+ expect(result.mappedDatasets.roads.style.opacity).toBe(0.5)
233
+ expect(result.actionsArray).toHaveLength(1)
234
+ expect(result.actionsArray[0]).toMatchObject({
235
+ method: 'applyDatasetOpacity',
236
+ parameters: ['roads']
237
+ })
238
+ })
239
+
240
+ it('logs error and returns unchanged state when dataset does not exist', () => {
241
+ const state = makeState()
242
+ const result = actions.SET_OPACITY(state, { datasetId: 'unknown', opacity: 0.5 })
243
+
244
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("'unknown'"))
245
+ expect(result).toBe(state)
246
+ })
247
+ })
248
+
249
+ // ─── SET_GLOBAL_OPACITY ───────────────────────────────────────────────────────
250
+
251
+ describe('SET_GLOBAL_OPACITY', () => {
252
+ it('updates globals.opacity and queues applyGlobalOpacity action', () => {
253
+ const state = makeState()
254
+ const result = actions.SET_GLOBAL_OPACITY(state, { opacity: 0.7 })
255
+
256
+ expect(result.globals.opacity).toBe(0.7)
257
+ expect(result.actionsArray).toHaveLength(1)
258
+ expect(result.actionsArray[0]).toMatchObject({
259
+ method: 'applyGlobalOpacity',
260
+ parameters: []
261
+ })
262
+ })
263
+ })
264
+
265
+ // ─── SET_GLOBAL_STATE ─────────────────────────────────────────────────────────
266
+
267
+ describe('SET_GLOBAL_STATE', () => {
268
+ it('merges payload into globals and queues applyGlobalOpacity action', () => {
269
+ const state = makeState()
270
+ const result = actions.SET_GLOBAL_STATE(state, { opacityMode: 'global' })
271
+
272
+ expect(result.globals.opacityMode).toBe('global')
273
+ expect(result.actionsArray).toHaveLength(1)
274
+ expect(result.actionsArray[0]).toMatchObject({ method: 'applyGlobalOpacity' })
275
+ })
276
+ })
277
+
278
+ // ─── HIDE_FEATURES ────────────────────────────────────────────────────────────
279
+
280
+ describe('HIDE_FEATURES', () => {
281
+ it('starts from empty hiddenFeatures when none are set', () => {
282
+ const state = makeStateWithDataset('roads')
283
+ const result = actions.HIDE_FEATURES(state, { datasetId: 'roads', featureIds: [5] })
284
+ expect(result.mappedDatasets.roads.hiddenFeatures).toEqual([5])
285
+ })
286
+
287
+ it('adds feature ids to hiddenFeatures and queues applyFeatureFilter', () => {
288
+ const state = makeStateWithDataset('roads', { hiddenFeatures: [1] })
289
+ const result = actions.HIDE_FEATURES(state, { datasetId: 'roads', featureIds: [2, 3] })
290
+
291
+ expect(result.mappedDatasets.roads.hiddenFeatures).toEqual([1, 2, 3])
292
+ expect(result.actionsArray[0]).toMatchObject({ method: 'applyFeatureFilter', parameters: ['roads'] })
293
+ })
294
+
295
+ it('deduplicates feature ids', () => {
296
+ const state = makeStateWithDataset('roads', { hiddenFeatures: [1, 2] })
297
+ const result = actions.HIDE_FEATURES(state, { datasetId: 'roads', featureIds: [2, 3] })
298
+
299
+ expect(result.mappedDatasets.roads.hiddenFeatures).toEqual([1, 2, 3])
300
+ })
301
+
302
+ it('logs error and returns unchanged state when dataset does not exist', () => {
303
+ const state = makeState()
304
+ const result = actions.HIDE_FEATURES(state, { datasetId: 'unknown', featureIds: [1] })
305
+
306
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("'unknown'"))
307
+ expect(result).toBe(state)
308
+ })
309
+ })
310
+
311
+ // ─── SHOW_FEATURES ────────────────────────────────────────────────────────────
312
+
313
+ describe('SHOW_FEATURES', () => {
314
+ it('handles dataset with no existing hiddenFeatures', () => {
315
+ const state = makeStateWithDataset('roads')
316
+ const result = actions.SHOW_FEATURES(state, { datasetId: 'roads', featureIds: [99] })
317
+ expect(result.mappedDatasets.roads.hiddenFeatures).toEqual([-1])
318
+ })
319
+
320
+ it('removes feature ids from hiddenFeatures and queues applyFeatureFilter', () => {
321
+ const state = makeStateWithDataset('roads', { hiddenFeatures: [1, 2, 3] })
322
+ const result = actions.SHOW_FEATURES(state, { datasetId: 'roads', featureIds: [2] })
323
+
324
+ expect(result.mappedDatasets.roads.hiddenFeatures).toEqual([1, 3])
325
+ expect(result.actionsArray[0]).toMatchObject({ method: 'applyFeatureFilter', parameters: ['roads'] })
326
+ })
327
+
328
+ it('sets hiddenFeatures to [-1] when all features are shown', () => {
329
+ const state = makeStateWithDataset('roads', { hiddenFeatures: [1] })
330
+ const result = actions.SHOW_FEATURES(state, { datasetId: 'roads', featureIds: [1] })
331
+
332
+ expect(result.mappedDatasets.roads.hiddenFeatures).toEqual([-1])
333
+ })
334
+
335
+ it('logs error and returns unchanged state when dataset does not exist', () => {
336
+ const state = makeState()
337
+ const result = actions.SHOW_FEATURES(state, { datasetId: 'unknown', featureIds: [1] })
338
+
339
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("'unknown'"))
340
+ expect(result).toBe(state)
341
+ })
342
+ })
343
+
344
+ // ─── REMOVE_ADAPTER_ACTIONS ───────────────────────────────────────────────────
345
+
346
+ describe('REMOVE_ADAPTER_ACTIONS', () => {
347
+ it('removes completed actions from actionsArray', () => {
348
+ const action1 = { method: 'addDataset', parameters: ['roads'], actionId: 0 }
349
+ const action2 = { method: 'applyStyle', parameters: ['roads', {}], actionId: 1 }
350
+ const state = { ...makeState(), actionsArray: [action1, action2] }
351
+
352
+ const result = actions.REMOVE_ADAPTER_ACTIONS(state, [action1])
353
+
354
+ expect(result.actionsArray).toEqual([action2])
355
+ })
356
+
357
+ it('returns unchanged state when completedActions is empty', () => {
358
+ const state = makeState()
359
+ const result = actions.REMOVE_ADAPTER_ACTIONS(state, [])
360
+ expect(result).toBe(state)
361
+ })
362
+
363
+ it('returns unchanged state when completedActions is undefined', () => {
364
+ const state = makeState()
365
+ const result = actions.REMOVE_ADAPTER_ACTIONS(state, undefined)
366
+ expect(result).toBe(state)
367
+ })
368
+ })
369
+
370
+ // ─── actionId counter behaviour ───────────────────────────────────────────────
371
+
372
+ describe('actionId counter', () => {
373
+ it('resets actionId to 0 when actionsArray is empty', () => {
374
+ const state = makeStateWithDataset('roads')
375
+ const result = actions.SET_DATASET_VISIBILITY(state, { datasetId: 'roads', visible: false })
376
+ expect(result.actionsArray[0].actionId).toBe(0)
377
+ })
378
+
379
+ it('increments actionId for each successive queued action', () => {
380
+ const state = makeStateWithDataset('roads')
381
+ const state1 = actions.SET_DATASET_VISIBILITY(state, { datasetId: 'roads', visible: false })
382
+ const state2 = actions.SET_DATASET_VISIBILITY(state1, { datasetId: 'roads', visible: true })
383
+
384
+ expect(state2.actionsArray[1].actionId).toBe(1)
385
+ })
386
+ })
@@ -0,0 +1,29 @@
1
+ import { mappedDatasetsReducer } from '../../reducers/mappedDatasetsReducer.js'
2
+ import { datasets as datasetDefinitions } from '../../reducers/__data__/demoDatasets.js'
3
+ import { attachGlobalState } from '../globalDataset.js'
4
+ const { datasetRegistry } = jest.requireActual('../datasetRegistry.js')
5
+ const { mappedDatasets, orderedDatasets } = mappedDatasetsReducer({ datasets: datasetDefinitions })
6
+
7
+ const globalState = {
8
+ opacityMode: 'dataset',
9
+ opacity: 1,
10
+ visible: true
11
+ }
12
+
13
+ // By adding jest.mock('<path-to>/datasetRegistry.js')
14
+ // to a test file, any import of datasetRegistry from that file will get this
15
+ // version with the demo datasets attached for testing.
16
+ // If we need to test against a different set of datasets
17
+ // we can import that config from demoDatasets.js (or roll our own)
18
+ // and attach it in the specific test
19
+ beforeEach(() => {
20
+ datasetRegistry.attach(mappedDatasets, orderedDatasets)
21
+ attachGlobalState(globalState)
22
+ })
23
+
24
+ datasetRegistry.mockExtend = (extraDatasets) => datasetRegistry.attach(
25
+ { ...mappedDatasets, ...extraDatasets },
26
+ [...orderedDatasets, ...Object.keys(extraDatasets)]
27
+ )
28
+
29
+ export { datasetRegistry }
@@ -0,0 +1,5 @@
1
+ import { Dataset } from './dataset.js'
2
+
3
+ export const createDataset = (datasetDefinition) => {
4
+ return new Dataset(datasetDefinition)
5
+ }
@@ -0,0 +1,172 @@
1
+ import { datasetRegistry } from './datasetRegistry.js'
2
+ import { hasCustomVisualStyle } from '../initialise/defaults.js'
3
+ import { hasPattern } from '../../../../../src/utils/patternUtils.js'
4
+ import { DynamicGeoJson } from './dynamicGeoJson.js'
5
+ import { calculateOpacity, getGlobalVisibility } from './globalDataset.js'
6
+
7
+ export class Dataset {
8
+ constructor (dataset) {
9
+ this._datasetDefinition = dataset
10
+ }
11
+
12
+ get id () { return this._datasetDefinition.id }
13
+ get label () { return this._datasetDefinition.label }
14
+ get hasSymbol () { return !this.hasSublayers && Boolean(this.style?.symbol) }
15
+ get hasPattern () { return !this.hasSublayers && !this.hasSymbol && hasPattern(this.style) }
16
+ get hasFill () { return !this.hasSublayers && !this.hasSymbol && (this.hasPattern || (this.style?.fill && this.style?.fill !== 'transparent')) }
17
+ get hasStroke () { return !this.hasSublayers && !this.hasSymbol && Boolean(this.style?.stroke) }
18
+ get tiles () { return this._datasetDefinition.tiles }
19
+ get geojson () { return this._datasetDefinition.geojson }
20
+ get idProperty () { return this._datasetDefinition.idProperty }
21
+ // TODO - handle transformRequest for non-dynamicGeoJSON as well (e.g. to add auth headers) --- IGNORE ---
22
+ // get transformRequest () { return this._datasetDefinition.transformRequest }
23
+ get parentId () { return this._datasetDefinition.parentId }
24
+ get minZoom () { return this._datasetDefinition.minZoom || this.parent?.minZoom }
25
+ get maxZoom () { return this._datasetDefinition.maxZoom || this.parent?.maxZoom }
26
+ get showInKey () { return this._datasetDefinition.showInKey || this.parent?.showInKey || false }
27
+ get groupLabel () { return this._datasetDefinition.groupLabel }
28
+
29
+ get opacity () {
30
+ const myOpacity = this.style?.opacity
31
+ const parentOpacity = this.parent?.style?.opacity
32
+ return calculateOpacity(myOpacity, parentOpacity)
33
+ }
34
+
35
+ get hasDynamicGeoJSON () {
36
+ return Boolean(this._datasetDefinition.dynamicGeoJSON)
37
+ }
38
+
39
+ get dynamicGeoJSON () {
40
+ return new DynamicGeoJson({
41
+ ...this._datasetDefinition.dynamicGeoJSON,
42
+ id: this.id,
43
+ minZoom: this.minZoom
44
+ })
45
+ }
46
+
47
+ get hiddenFeatures () { return this._datasetDefinition.hiddenFeatures }
48
+ get hasHiddenFeatures () { return Boolean(this.hiddenFeatures?.length > 0 || this.parent?.hasHiddenFeatures) }
49
+
50
+ get filter () {
51
+ return null // TODO - implement filter construction for esri and openLayers adapters
52
+ }
53
+
54
+ // Returns true if either the parent (if it has one) or global visibility is hidden , otherwise returns true.
55
+ // This is used to determine whether to show a tooltip
56
+ // TODO: also work out how to convey datasets hidden by zoom filter.
57
+ get isHiddenByInheritance () {
58
+ if (this.isSublayer) {
59
+ return !this.parent?.visible
60
+ }
61
+ return !getGlobalVisibility()
62
+ }
63
+
64
+ // Returns true if this dataset is visible based on its own visibility setting,
65
+ // even if globalVisibility is false or its parent is not visible.
66
+ // This is used to determine whether the dataset is rendered as checked when
67
+ // disabled in the layers menu.
68
+ get isLocallyVisible () {
69
+ if (this.isVisible) {
70
+ return true
71
+ }
72
+ return this._datasetDefinition.visible
73
+ }
74
+
75
+ get visible () {
76
+ if (this.isSublayer) {
77
+ return this._datasetDefinition.visible && (this.parent?.visible)
78
+ }
79
+ return this._datasetDefinition.visible && getGlobalVisibility()
80
+ }
81
+
82
+ get symbolAnchor () {
83
+ if (this.style?.symbolAnchor) {
84
+ return this.style.symbolAnchor
85
+ }
86
+ return this.parent?.symbolAnchor
87
+ }
88
+
89
+ get sourceLayer () {
90
+ if (this.isSublayer) {
91
+ return this.parent.sourceLayer
92
+ }
93
+ if (this.tiles) {
94
+ return this._datasetDefinition.sourceLayer
95
+ }
96
+ return undefined
97
+ }
98
+
99
+ get isSublayer () {
100
+ return Boolean(this._datasetDefinition.parentId)
101
+ }
102
+
103
+ get hasSublayers () {
104
+ return this._datasetDefinition.sublayerIds?.length > 0
105
+ }
106
+
107
+ get sublayerIds () {
108
+ return this._datasetDefinition.sublayerIds
109
+ }
110
+
111
+ get sublayers () {
112
+ const { sublayerIds } = this
113
+ if (sublayerIds) {
114
+ return sublayerIds.map(id => datasetRegistry.getDataset(id))
115
+ }
116
+ return undefined
117
+ }
118
+
119
+ get parent () {
120
+ if (this._datasetDefinition.parentId) {
121
+ return datasetRegistry.getDataset(this._datasetDefinition.parentId)
122
+ }
123
+ return undefined
124
+ }
125
+
126
+ get style () {
127
+ const parentStyle = this.parent?.style
128
+ if (parentStyle) {
129
+ // Here - we must set the merge styles opacity to undefined before the specific child opacity
130
+ // so that we can correctly calculate opacity in the Dataset.opacity getter
131
+ // - otherwise if opacity mode multiply is set,
132
+ // any child with a parent opacity only would be multiplied by itself
133
+ return {
134
+ ...parentStyle,
135
+ opacity: undefined,
136
+ ...this._datasetDefinition.style,
137
+ symbolDescription: this.symbolDescription
138
+ }
139
+ }
140
+ return this._datasetDefinition.style || {}
141
+ }
142
+
143
+ get hasCustomVisualStyle () {
144
+ return hasCustomVisualStyle(this._datasetDefinition.style || {})
145
+ }
146
+
147
+ get symbolDescription () {
148
+ if (this._datasetDefinition.style?.symbolDescription) {
149
+ return this._datasetDefinition.style.symbolDescription
150
+ }
151
+ if (this.hasCustomVisualStyle) {
152
+ return undefined
153
+ }
154
+ return this.parent?.symbolDescription
155
+ }
156
+
157
+ get patternConfigs () {
158
+ const stylePatterns = this.hasPattern ? [this.style] : []
159
+ if (this.hasSublayers) {
160
+ return [...stylePatterns, ...this.sublayers.flatMap(sublayer => sublayer.patternConfigs)]
161
+ }
162
+ return stylePatterns
163
+ }
164
+
165
+ get symbolConfigs () {
166
+ const styleSymbols = this.hasSymbol ? [this.style] : []
167
+ if (this.hasSublayers) {
168
+ return [...styleSymbols, ...this.sublayers.flatMap(sublayer => sublayer.symbolConfigs)]
169
+ }
170
+ return styleSymbols
171
+ }
172
+ }