@defra/interactive-map 0.0.14-alpha → 0.0.16-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 (199) hide show
  1. package/assets/css/docusaurus.css +104 -0
  2. package/assets/images/favicon.svg +1 -0
  3. package/assets/images/hero.png +0 -0
  4. package/dist/css/index.css +1 -1
  5. package/dist/esm/im-core.js +1 -1
  6. package/dist/umd/im-core.js +1 -1
  7. package/dist/umd/index.js +1 -1
  8. package/docs/api/slot-map.svg +1 -0
  9. package/docs/api/slots.md +89 -6
  10. package/docs/api.md +1 -1
  11. package/docs/architecture.md +3 -1
  12. package/docs/{demo.mdx → examples.mdx} +1 -1
  13. package/docs/getting-started.md +1 -3
  14. package/docs/index.mdx +42 -0
  15. package/docs/plugins/interact.md +176 -55
  16. package/docs/plugins/map-styles.md +64 -7
  17. package/docs/plugins/search.md +207 -63
  18. package/docs/plugins.md +7 -15
  19. package/docusaurus.config.cjs +34 -34
  20. package/jest.setup.js +1 -1
  21. package/package.json +5 -4
  22. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  23. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  24. package/plugins/beta/datasets/src/DatasetsInit.jsx +1 -1
  25. package/plugins/beta/datasets/src/api/addDataset.js +1 -1
  26. package/plugins/beta/datasets/src/api/hideDataset.js +1 -1
  27. package/plugins/beta/datasets/src/api/hideFeatures.js +1 -1
  28. package/plugins/beta/datasets/src/api/removeDataset.js +1 -1
  29. package/plugins/beta/datasets/src/api/showDataset.js +1 -1
  30. package/plugins/beta/datasets/src/api/showFeatures.js +1 -1
  31. package/plugins/beta/datasets/src/datasets.js +4 -4
  32. package/plugins/beta/datasets/src/defaults.js +1 -1
  33. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +5 -5
  34. package/plugins/beta/datasets/src/handleSetMapStyle.js +1 -1
  35. package/plugins/beta/datasets/src/manifest.js +7 -7
  36. package/plugins/beta/datasets/src/mapLayers.js +2 -3
  37. package/plugins/beta/datasets/src/panels/Key.jsx +31 -29
  38. package/plugins/beta/datasets/src/panels/Layers.jsx +8 -9
  39. package/plugins/beta/datasets/src/utils/bbox.js +4 -4
  40. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  41. package/plugins/beta/draw-es/src/DrawInit.jsx +16 -16
  42. package/plugins/beta/draw-es/src/api/addFeature.js +3 -3
  43. package/plugins/beta/draw-es/src/api/deleteFeature.js +3 -3
  44. package/plugins/beta/draw-es/src/api/editFeature.js +3 -3
  45. package/plugins/beta/draw-es/src/api/newPolygon.js +3 -3
  46. package/plugins/beta/draw-es/src/events.js +52 -20
  47. package/plugins/beta/draw-es/src/events.test.js +301 -0
  48. package/plugins/beta/draw-es/src/graphic.js +1 -1
  49. package/plugins/beta/draw-es/src/manifest.js +4 -4
  50. package/plugins/beta/draw-es/src/reducer.js +1 -1
  51. package/plugins/beta/draw-es/src/sketchViewModel.js +1 -1
  52. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  53. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  54. package/plugins/beta/draw-ml/src/DrawInit.jsx +49 -52
  55. package/plugins/beta/draw-ml/src/api/deleteFeature.js +1 -1
  56. package/plugins/beta/draw-ml/src/api/editFeature.js +8 -5
  57. package/plugins/beta/draw-ml/src/api/newLine.js +0 -1
  58. package/plugins/beta/draw-ml/src/api/newPolygon.js +0 -1
  59. package/plugins/beta/draw-ml/src/api/split.js +4 -4
  60. package/plugins/beta/draw-ml/src/defaults.js +1 -1
  61. package/plugins/beta/draw-ml/src/events.js +8 -6
  62. package/plugins/beta/draw-ml/src/manifest.js +15 -15
  63. package/plugins/beta/draw-ml/src/mapboxDraw.js +1 -1
  64. package/plugins/beta/draw-ml/src/mapboxSnap.js +17 -18
  65. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -31
  66. package/plugins/beta/draw-ml/src/modes/disabledMode.js +1 -1
  67. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +11 -11
  68. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +7 -7
  69. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +8 -8
  70. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +7 -7
  71. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +32 -24
  72. package/plugins/beta/draw-ml/src/reducer.js +1 -1
  73. package/plugins/beta/draw-ml/src/undoStack.js +4 -4
  74. package/plugins/beta/draw-ml/src/utils/snapHelpers.js +12 -12
  75. package/plugins/beta/draw-ml/src/utils/spatial.js +11 -11
  76. package/plugins/beta/frame/src/Frame.jsx +4 -4
  77. package/plugins/beta/frame/src/FrameInit.jsx +4 -4
  78. package/plugins/beta/frame/src/api/addFrame.js +1 -1
  79. package/plugins/beta/frame/src/api/editFeature.js +1 -1
  80. package/plugins/beta/frame/src/config.js +1 -1
  81. package/plugins/beta/frame/src/manifest.js +3 -3
  82. package/plugins/beta/frame/src/reducer.js +1 -1
  83. package/plugins/beta/frame/src/utils.js +1 -1
  84. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  85. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  86. package/plugins/beta/map-styles/src/MapStyles.jsx +18 -18
  87. package/plugins/beta/map-styles/src/manifest.js +2 -2
  88. package/plugins/beta/scale-bar/src/ScaleBar.jsx +5 -5
  89. package/plugins/beta/use-location/src/UseLocation.jsx +1 -1
  90. package/plugins/beta/use-location/src/defaults.js +1 -1
  91. package/plugins/beta/use-location/src/events.js +3 -3
  92. package/plugins/interact/src/InteractInit.jsx +1 -2
  93. package/plugins/interact/src/api/enable.js +8 -5
  94. package/plugins/interact/src/api/enable.test.js +2 -2
  95. package/plugins/interact/src/api/selectFeature.js +4 -4
  96. package/plugins/interact/src/api/unselectFeature.js +5 -5
  97. package/plugins/interact/src/defaults.js +0 -1
  98. package/plugins/interact/src/events.test.js +15 -15
  99. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  100. package/plugins/interact/src/hooks/useInteractionHandlers.js +2 -2
  101. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -5
  102. package/plugins/interact/src/manifest.js +2 -2
  103. package/plugins/interact/src/manifest.test.js +3 -4
  104. package/plugins/interact/src/reducer.js +3 -3
  105. package/plugins/interact/src/reducer.test.js +0 -1
  106. package/plugins/interact/src/utils/spatial.js +10 -10
  107. package/plugins/interact/src/utils/spatial.test.js +14 -14
  108. package/plugins/search/dist/css/index.css +1 -1
  109. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  110. package/plugins/search/dist/esm/index.js +1 -1
  111. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  112. package/plugins/search/dist/umd/index.js +1 -1
  113. package/plugins/search/src/Search.jsx +7 -6
  114. package/plugins/search/src/Search.test.jsx +23 -23
  115. package/plugins/search/src/components/CloseButton/CloseButton.jsx +15 -15
  116. package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +2 -2
  117. package/plugins/search/src/components/Form/Form.jsx +14 -14
  118. package/plugins/search/src/components/Form/Form.test.jsx +11 -11
  119. package/plugins/search/src/components/OpenButton/OpenButton.jsx +16 -15
  120. package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +6 -2
  121. package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +15 -15
  122. package/plugins/search/src/components/Suggestions/Suggestions.jsx +6 -6
  123. package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +4 -4
  124. package/plugins/search/src/datasets.js +12 -13
  125. package/plugins/search/src/datasets.test.js +1 -1
  126. package/plugins/search/src/defaults.js +1 -1
  127. package/plugins/search/src/events/fetchSuggestions.js +4 -4
  128. package/plugins/search/src/events/fetchSuggestions.test.js +5 -5
  129. package/plugins/search/src/events/formHandlers.js +3 -3
  130. package/plugins/search/src/events/formHandlers.test.js +1 -1
  131. package/plugins/search/src/events/index.js +2 -2
  132. package/plugins/search/src/events/index.test.js +2 -2
  133. package/plugins/search/src/events/inputHandlers.js +4 -4
  134. package/plugins/search/src/events/inputHandlers.test.js +1 -1
  135. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  136. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  137. package/plugins/search/src/index.js +2 -1
  138. package/plugins/search/src/index.test.js +3 -3
  139. package/plugins/search/src/manifest.js +6 -4
  140. package/plugins/search/src/reducer.js +1 -2
  141. package/plugins/search/src/reducer.test.js +2 -2
  142. package/plugins/search/src/search.scss +18 -6
  143. package/plugins/search/src/utils/parseOsNamesResults.js +1 -2
  144. package/plugins/search/src/utils/parseOsNamesResults.test.js +2 -2
  145. package/plugins/search/src/utils/updateMap.js +1 -1
  146. package/plugins/search/src/utils/updateMap.test.js +5 -5
  147. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  148. package/providers/beta/esri/src/esriProvider.js +5 -5
  149. package/providers/beta/esri/src/utils/coords.js +1 -1
  150. package/providers/beta/esri/src/utils/esriFixes.js +1 -1
  151. package/providers/beta/esri/src/utils/query.js +4 -4
  152. package/providers/beta/esri/src/utils/spatial.js +1 -2
  153. package/providers/beta/esri/src/utils/spatial.test.js +4 -1
  154. package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +1 -1
  155. package/providers/maplibre/src/appEvents.test.js +1 -1
  156. package/providers/maplibre/src/index.js +1 -1
  157. package/providers/maplibre/src/index.test.js +3 -5
  158. package/providers/maplibre/src/mapEvents.test.js +15 -5
  159. package/providers/maplibre/src/maplibreProvider.test.js +6 -2
  160. package/providers/maplibre/src/utils/calculateLinearTextSize.js +4 -4
  161. package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +3 -3
  162. package/providers/maplibre/src/utils/detectWebgl.test.js +1 -1
  163. package/providers/maplibre/src/utils/highlightFeatures.js +2 -2
  164. package/providers/maplibre/src/utils/highlightFeatures.test.js +12 -6
  165. package/providers/maplibre/src/utils/labels.js +19 -20
  166. package/providers/maplibre/src/utils/labels.test.js +15 -13
  167. package/providers/maplibre/src/utils/maplibreFixes.test.js +1 -1
  168. package/providers/maplibre/src/utils/queryFeatures.js +6 -6
  169. package/providers/maplibre/src/utils/queryFeatures.test.js +13 -13
  170. package/providers/maplibre/src/utils/spatial.js +0 -1
  171. package/providers/maplibre/src/utils/spatial.test.js +26 -27
  172. package/src/App/components/Panel/Panel.module.scss +1 -0
  173. package/src/App/hooks/useLayoutMeasurements.js +1 -10
  174. package/src/App/hooks/useLayoutMeasurements.test.js +2 -5
  175. package/src/App/hooks/useVisibleGeometry.js +7 -13
  176. package/src/App/hooks/useVisibleGeometry.test.js +72 -47
  177. package/src/App/layout/Layout.jsx +0 -3
  178. package/src/App/layout/Layout.test.jsx +0 -1
  179. package/src/App/layout/layout.module.scss +11 -77
  180. package/src/App/registry/pluginRegistry.js +17 -0
  181. package/src/App/registry/pluginRegistry.test.js +33 -0
  182. package/src/App/renderer/HtmlElementHost.jsx +0 -1
  183. package/src/App/renderer/HtmlElementHost.test.jsx +20 -11
  184. package/src/App/renderer/mapButtons.js +3 -2
  185. package/src/App/renderer/mapPanels.test.js +3 -3
  186. package/src/App/renderer/slotHelpers.js +2 -2
  187. package/src/App/renderer/slotHelpers.test.js +3 -3
  188. package/src/App/renderer/slots.js +0 -3
  189. package/src/App/store/AppProvider.jsx +0 -1
  190. package/src/App/store/appDispatchMiddleware.js +33 -1
  191. package/src/App/store/appDispatchMiddleware.test.js +250 -222
  192. package/src/config/appConfig.js +4 -4
  193. package/src/utils/getSafeZoneInset.js +139 -42
  194. package/src/utils/getSafeZoneInset.test.js +298 -122
  195. package/src/utils/logger.js +6 -0
  196. package/src/utils/logger.test.js +32 -0
  197. package/webpack.dev.mjs +22 -18
  198. package/docs/govuk-prototype.md +0 -23
  199. package/docs/index.md +0 -19
@@ -2,14 +2,14 @@ import { useEffect } from 'react'
2
2
  import { createSketchViewModel } from './sketchViewModel.js'
3
3
  import { attachEvents } from './events.js'
4
4
 
5
- export const DrawInit = ({
6
- appState,
7
- mapState,
8
- pluginConfig,
9
- pluginState,
10
- services,
11
- mapProvider,
12
- buttonConfig
5
+ export const DrawInit = ({
6
+ appState,
7
+ mapState,
8
+ pluginConfig,
9
+ pluginState,
10
+ services,
11
+ mapProvider,
12
+ buttonConfig
13
13
  }) => {
14
14
  const { events, eventBus } = services
15
15
  const { mapColorScheme } = mapState.mapStyle || {}
@@ -22,9 +22,9 @@ export const DrawInit = ({
22
22
  // Initialize sketch components once
23
23
  useEffect(() => {
24
24
  if (!isActive || mapProvider.sketchViewModel) {
25
- return
26
- }
27
-
25
+ return
26
+ }
27
+
28
28
  const { sketchViewModel, sketchLayer, emptySketchLayer } = createSketchViewModel({
29
29
  pluginState,
30
30
  mapProvider,
@@ -33,21 +33,21 @@ export const DrawInit = ({
33
33
 
34
34
  mapProvider.sketchViewModel = sketchViewModel
35
35
  mapProvider.sketchLayer = sketchLayer
36
- mapProvider.emptySketchLayer = emptySketchLayer
36
+ mapProvider.emptySketchLayer = emptySketchLayer
37
37
  eventBus.emit('draw:ready')
38
38
 
39
39
  return () => {
40
40
  mapProvider.sketchViewModel = null
41
41
  mapProvider.sketchLayer = null
42
- mapProvider.emptySketchLayer = null
42
+ mapProvider.emptySketchLayer = null
43
43
  }
44
44
  }, [mapState.isMapReady, appState.mode])
45
45
 
46
46
  // Attach/detach events
47
47
  useEffect(() => {
48
48
  if (!isActive || !mapProvider.sketchViewModel) {
49
- return
50
- }
49
+ return
50
+ }
51
51
 
52
52
  const cleanup = attachEvents({
53
53
  pluginState,
@@ -62,4 +62,4 @@ export const DrawInit = ({
62
62
  cleanup()
63
63
  }
64
64
  }, [isActive, mapColorScheme, pluginState])
65
- }
65
+ }
@@ -7,7 +7,7 @@ export const addFeature = ({ pluginState, mapState, mapProvider, services }, fea
7
7
  const { eventBus } = services
8
8
 
9
9
  const graphic = createGraphic(feature.id, feature.geometry.coordinates, mapStyle.mapColorScheme)
10
-
10
+
11
11
  // Add the graphic to the layer
12
12
  sketchLayer.add(graphic)
13
13
 
@@ -15,7 +15,7 @@ export const addFeature = ({ pluginState, mapState, mapProvider, services }, fea
15
15
  sketchViewModel.layer = emptySketchLayer
16
16
 
17
17
  // Store initial feature in plugin state
18
- dispatch({ type: 'SET_FEATURE', payload: { feature }})
18
+ dispatch({ type: 'SET_FEATURE', payload: { feature } })
19
19
 
20
20
  eventBus.emit('draw:add', feature)
21
- }
21
+ }
@@ -10,13 +10,13 @@ export const deleteFeature = ({ pluginState, mapProvider, services }, featureId)
10
10
  sketchViewModel.cancel()
11
11
  sketchLayer.remove(graphic)
12
12
  sketchViewModel.layer = emptySketchLayer
13
-
13
+
14
14
  // Reset state
15
- dispatch({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null }})
15
+ dispatch({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null } })
16
16
 
17
17
  // Emit event
18
18
  eventBus.emit('draw:delete', { featureId })
19
19
 
20
20
  // Clear mode
21
21
  dispatch({ type: 'SET_MODE', payload: null })
22
- }
22
+ }
@@ -18,12 +18,12 @@ export const editFeature = ({ pluginState, mapProvider }, featureId) => {
18
18
  tool: 'reshape',
19
19
  toggleToolOnClick: false,
20
20
  enableRotation: false,
21
- enableScaling: false
21
+ enableScaling: false
22
22
  })
23
23
 
24
24
  // Set original feature
25
25
  const feature = graphicToGeoJSON(graphic)
26
- dispatch({ type: 'SET_FEATURE', payload: { feature }})
26
+ dispatch({ type: 'SET_FEATURE', payload: { feature } })
27
27
 
28
28
  dispatch({ type: 'SET_MODE', payload: 'edit-feature' })
29
- }
29
+ }
@@ -12,7 +12,7 @@ export const newPolygon = ({ mapState, pluginState, mapProvider, services }, fea
12
12
  const handleCreateComplete = sketchViewModel.on('create', (e) => {
13
13
  if (e.state === 'complete') {
14
14
  e.graphic.attributes = { id: featureId }
15
-
15
+
16
16
  // Fix: to address calling some sketchViewModel methods syncronously
17
17
  requestAnimationFrame(() => {
18
18
  sketchViewModel.update(e.graphic, {
@@ -24,7 +24,7 @@ export const newPolygon = ({ mapState, pluginState, mapProvider, services }, fea
24
24
  // Store temp feature in state and emit create
25
25
  const tempFeature = graphicToGeoJSON(e.graphic)
26
26
  eventBus.emit('draw:created', tempFeature)
27
- dispatch({ type: 'SET_FEATURE', payload: { tempFeature }})
27
+ dispatch({ type: 'SET_FEATURE', payload: { tempFeature } })
28
28
 
29
29
  handleCreateComplete.remove()
30
30
  }
@@ -34,4 +34,4 @@ export const newPolygon = ({ mapState, pluginState, mapProvider, services }, fea
34
34
  sketchViewModel.create('polygon')
35
35
 
36
36
  dispatch({ type: 'SET_MODE', payload: 'new-polygon' })
37
- }
37
+ }
@@ -1,9 +1,9 @@
1
- import * as simplifyOperator from "@arcgis/core/geometry/operators/simplifyOperator.js"
1
+ import * as simplifyOperator from '@arcgis/core/geometry/operators/simplifyOperator.js'
2
2
  import { createGraphic, createSymbol, graphicToGeoJSON } from './graphic.js'
3
3
 
4
4
  const MODE_CHANGE_DELAY = 50
5
5
 
6
- export function attachEvents({ pluginState, mapProvider, events, eventBus, buttonConfig, mapColorScheme }) {
6
+ export function attachEvents ({ pluginState, mapProvider, events, eventBus, buttonConfig, mapColorScheme }) {
7
7
  const { view, sketchViewModel, sketchLayer, emptySketchLayer } = mapProvider
8
8
 
9
9
  if (!sketchViewModel) {
@@ -12,36 +12,36 @@ export function attachEvents({ pluginState, mapProvider, events, eventBus, butto
12
12
 
13
13
  const { drawDone, drawCancel } = buttonConfig
14
14
  const { dispatch, mode, feature } = pluginState
15
-
15
+
16
16
  // Re-colour graphics when map style changes
17
17
  const reColour = async () => {
18
18
  const activeGraphicId = pluginState.feature?.properties?.id
19
19
  let activeGraphic = null
20
20
  const isCreating = sketchViewModel.state === 'active' && !activeGraphicId
21
-
21
+
22
22
  // Cancel and wait, but only if we're in update mode (not create mode)
23
23
  if (sketchViewModel.state === 'active' && activeGraphicId) {
24
24
  sketchViewModel.cancel()
25
25
  await new Promise(resolve => setTimeout(resolve, MODE_CHANGE_DELAY))
26
26
  }
27
-
27
+
28
28
  // Update the default symbol for new polygons
29
29
  sketchViewModel.polygonSymbol = createSymbol(mapColorScheme)
30
-
30
+
31
31
  // Update existing graphics
32
32
  sketchLayer?.graphics.items.forEach(graphic => {
33
33
  const newGraphic = createGraphic(
34
- graphic.attributes.id,
35
- graphic.geometry.rings,
34
+ graphic.attributes.id,
35
+ graphic.geometry.rings,
36
36
  mapColorScheme
37
37
  )
38
38
  graphic.symbol = newGraphic.symbol
39
-
39
+
40
40
  if (activeGraphicId === graphic.attributes.id) {
41
41
  activeGraphic = graphic
42
42
  }
43
43
  })
44
-
44
+
45
45
  // Re-enter update mode only if we were editing (not creating)
46
46
  if (activeGraphic && !isCreating && sketchViewModel.layer === sketchLayer) {
47
47
  try {
@@ -68,10 +68,19 @@ export function attachEvents({ pluginState, mapProvider, events, eventBus, butto
68
68
  // Event handlers
69
69
  const handleMapStyleChange = () => reColour()
70
70
 
71
+ const onGraphicChanged = (graphic) => {
72
+ if (!graphic) {
73
+ return
74
+ }
75
+ const tempFeature = graphicToGeoJSON(graphic)
76
+ eventBus.emit('draw:updated', tempFeature)
77
+ dispatch({ type: 'SET_FEATURE', payload: { tempFeature } })
78
+ }
79
+
71
80
  const handleSketchUpdate = (e) => {
72
81
  const toolInfoType = e.toolEventInfo?.type
73
82
  const graphic = e.graphics[0]
74
-
83
+
75
84
  // Prevent polygon move
76
85
  if (toolInfoType === 'move-start') {
77
86
  sketchViewModel.cancel()
@@ -87,10 +96,8 @@ export function attachEvents({ pluginState, mapProvider, events, eventBus, butto
87
96
  }
88
97
 
89
98
  // Emit event on update
90
- if (toolInfoType === 'reshape-stop') {
91
- const tempFeature = graphicToGeoJSON(graphic)
92
- eventBus.emit('draw:updated', tempFeature)
93
- dispatch({ type: 'SET_FEATURE', payload: { tempFeature }})
99
+ if (toolInfoType === 'reshape-stop' || toolInfoType === 'vertex-remove') {
100
+ onGraphicChanged(graphic)
94
101
  }
95
102
  }
96
103
 
@@ -108,17 +115,38 @@ export function attachEvents({ pluginState, mapProvider, events, eventBus, butto
108
115
  updateGraphic()
109
116
  }
110
117
 
118
+ const handleCreate = async (event) => {
119
+ const { toolEventInfo } = event
120
+ const graphic = event?.graphic
121
+ if (graphic && toolEventInfo?.type === 'vertex-add') {
122
+ const rings = graphic.geometry?.rings
123
+ // rings.length is > 1 occurs when the shape becomes complex (ie self intersects)
124
+ // setTimeout is required to cause the undo to be called after handleCreate completes
125
+ // otherwise the previous change, rather than this one, is undone
126
+ if (rings?.length > 1) {
127
+ setTimeout(() => sketchViewModel.undo(), 0)
128
+ } else if (rings?.[0]?.length > 3) {
129
+ onGraphicChanged(graphic) // emit a graphic update on draw, once the graphic is 2D
130
+ }
131
+ }
132
+ }
133
+
134
+ const handleUndo = async (event) => {
135
+ const graphic = event?.graphics?.[0]
136
+ onGraphicChanged(graphic)
137
+ }
138
+
111
139
  const handleDone = () => {
112
140
  sketchViewModel.cancel()
113
141
  sketchViewModel.layer = emptySketchLayer
114
142
  dispatch({ type: 'SET_MODE', payload: null })
115
- dispatch({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null }})
143
+ dispatch({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null } })
116
144
  eventBus.emit('draw:done', { newFeature: pluginState.tempFeature })
117
145
  }
118
146
 
119
147
  const handleCancel = () => {
120
148
  sketchViewModel.cancel()
121
-
149
+
122
150
  // Clear all graphics
123
151
  sketchLayer.removeAll()
124
152
 
@@ -143,10 +171,12 @@ export function attachEvents({ pluginState, mapProvider, events, eventBus, butto
143
171
  eventBus.on(events.MAP_STYLE_CHANGE, handleMapStyleChange)
144
172
  const sketchUpdateHandler = sketchViewModel.on('update', handleSketchUpdate)
145
173
  const viewClickHandler = view.on('click', handleViewClick)
146
-
174
+ const createHandler = sketchViewModel.on('create', handleCreate)
175
+ const undoHandler = sketchViewModel.on('undo', handleUndo)
176
+
147
177
  const prevDoneClick = drawDone.onClick
148
178
  const prevCancelClick = drawCancel.onClick
149
-
179
+
150
180
  drawDone.onClick = handleDone
151
181
  drawCancel.onClick = handleCancel
152
182
 
@@ -155,7 +185,9 @@ export function attachEvents({ pluginState, mapProvider, events, eventBus, butto
155
185
  eventBus.off(events.MAP_STYLE_CHANGE, handleMapStyleChange)
156
186
  sketchUpdateHandler.remove()
157
187
  viewClickHandler.remove()
188
+ createHandler.remove()
189
+ undoHandler.remove()
158
190
  drawDone.onClick = prevDoneClick
159
191
  drawCancel.onClick = prevCancelClick
160
192
  }
161
- }
193
+ }
@@ -0,0 +1,301 @@
1
+ import { attachEvents } from './events.js'
2
+ import { EVENTS as events } from '../../../../src/config/events.js'
3
+
4
+ import * as graphicJs from './graphic.js'
5
+ jest.mock('./graphic.js')
6
+ const createGraphic = jest.spyOn(graphicJs, 'createGraphic')
7
+ const createSymbol = jest.spyOn(graphicJs, 'createSymbol')
8
+ // const graphicToGeoJSON = jest.spyOn(graphicJs, 'graphicToGeoJSON')
9
+
10
+ const dispatch = jest.fn()
11
+
12
+ const createMockEventHandler = (type) => {
13
+ const callbackSpies = {}
14
+ const removeSpy = jest.fn()
15
+ const onSpy = jest.fn((eventType, callback) => {
16
+ callbackSpies[eventType] = callback
17
+ return { remove: () => removeSpy(eventType) }
18
+ })
19
+
20
+ // returns an async function that runs asserts
21
+ const assertOnCalls = (methodArray) => async () => {
22
+ expect(onSpy.mock.calls, `${type}.on should be called ${methodArray.length} times`)
23
+ .toHaveLength(methodArray.length)
24
+ methodArray.forEach((method) =>
25
+ expect(onSpy, `${type}.on should be called with ${method} and a callback`)
26
+ .toHaveBeenCalledWith(method, callbackSpies[method]))
27
+ }
28
+
29
+ const assertRemoveCalls = (methodArray) => async () => {
30
+ expect(removeSpy.mock.calls, `${type}.remove/off should be called ${methodArray.length} times`)
31
+ .toHaveLength(methodArray.length)
32
+ methodArray.forEach((method) => {
33
+ const removeParams = type === 'eventBus' ? [method, callbackSpies[[method]]] : [method]
34
+ expect(removeSpy, `${type}.remove/off should be called with ${removeParams} `).toHaveBeenCalledWith(...removeParams)
35
+ })
36
+ }
37
+
38
+ return {
39
+ removeSpy,
40
+ callbackSpies,
41
+ emit: jest.fn(),
42
+ off: removeSpy,
43
+ on: onSpy,
44
+ assertOnCalls,
45
+ assertRemoveCalls,
46
+ triggerEvent: (eventType, event) => callbackSpies[eventType](event)
47
+ }
48
+ }
49
+ const coordinates = [[[337560, 504846], [337580, 504855], [337587, 504838], [337565, 504833], [337560, 504846]]]
50
+ const feature = {
51
+ type: 'Feature',
52
+ geometry: {
53
+ type: 'Polygon',
54
+ coordinates
55
+ },
56
+ properties: { id: 'boundary' }
57
+ }
58
+
59
+ const mockSymbol = { color: 'blue' }
60
+ const newGraphicMock = { symbol: { color: 'red' } }
61
+
62
+ const mockGraphic = {
63
+ attributes: { id: 'boundary' },
64
+ geometry: { rings: coordinates },
65
+ symbol: null
66
+ }
67
+
68
+ const sketchLayer = {
69
+ removeAll: jest.fn(),
70
+ add: jest.fn(),
71
+ graphics: { items: [mockGraphic] }
72
+ }
73
+
74
+ class ButtonConfigMock {
75
+ constructor (name) {
76
+ this.name = name
77
+ this._onClick = 'Initial Value'
78
+ this._initialOnClick = this._onClick
79
+ this.assignOnClickSpy = jest.spyOn(this, 'onClick', 'set')
80
+ }
81
+
82
+ set onClick (onClick) {
83
+ this._onClick = onClick
84
+ }
85
+
86
+ get onClick () {
87
+ return this._onClick
88
+ }
89
+
90
+ assertOnClickAssignment () {
91
+ return async () => {
92
+ expect(this.assignOnClickSpy.mock.calls, `${this.name}.onClick should have been reassigned once`)
93
+ .toHaveLength(1)
94
+ expect(this._onClick, `${this.name}.onClick should have changed`)
95
+ .not.toEqual(this._initialOnClick)
96
+ }
97
+ }
98
+
99
+ assertOnClickReset () {
100
+ return async () => {
101
+ expect(this.assignOnClickSpy.mock.calls, `${this.name}.onClick should have been assigned twice`)
102
+ .toHaveLength(2)
103
+ expect(this._onClick, `${this.name}.onClick should been set back to its initial value`)
104
+ .toEqual(this._initialOnClick)
105
+ }
106
+ }
107
+ }
108
+ const emptySketchLayer = {}
109
+
110
+ const buildParams = (overrides = {}) => {
111
+ return {
112
+ pluginState: {
113
+ dispatch,
114
+ mode: 'new-polygon', // or: edit-feature
115
+ feature,
116
+ ...overrides.pluginState
117
+ },
118
+ mapProvider: {
119
+ view: createMockEventHandler('view'),
120
+ sketchViewModel: {
121
+ ...createMockEventHandler('sketchViewModel'),
122
+ layer: sketchLayer,
123
+ cancel: jest.fn(),
124
+ state: 'idle',
125
+ polygonSymbol: null,
126
+ update: jest.fn().mockResolvedValue(undefined)
127
+ },
128
+ // sketchViewModel: { ...sketchViewModel, ...overrides.sketchViewModel },
129
+ sketchLayer,
130
+ emptySketchLayer,
131
+ ...overrides.mapProvider
132
+ },
133
+ events,
134
+ eventBus: createMockEventHandler('eventBus'),
135
+ buttonConfig: {
136
+ drawDone: new ButtonConfigMock('Done'),
137
+ drawCancel: new ButtonConfigMock('Cancel')
138
+ },
139
+ mapColorScheme: 'MOCK_COLOUR_SCHEME'
140
+ }
141
+ }
142
+
143
+ describe('attachEvents - draw-es', () => {
144
+ beforeEach(() => {
145
+ jest.useFakeTimers()
146
+ })
147
+
148
+ afterEach(() => {
149
+ jest.useRealTimers()
150
+ })
151
+
152
+ describe('listeners', () => {
153
+ const params = buildParams()
154
+ const { drawDone, drawCancel } = params.buttonConfig
155
+ const { eventBus } = params
156
+ const { sketchViewModel, view } = params.mapProvider
157
+ const teardown = attachEvents(params)
158
+ describe('attach', () => {
159
+ it('should add view listeners', view.assertOnCalls(['click']))
160
+ it('should add sketchViewModel listeners', sketchViewModel.assertOnCalls(['update', 'create', 'undo']))
161
+ it('should add eventBus listeners', eventBus.assertOnCalls([events.MAP_STYLE_CHANGE]))
162
+ it('should assign a Done click handler', drawDone.assertOnClickAssignment())
163
+ it('should assign a Cancel click handler', drawCancel.assertOnClickAssignment())
164
+ })
165
+
166
+ describe('teardown', () => {
167
+ beforeAll(teardown)
168
+ it('should teardown the view listeners', view.assertRemoveCalls(['click']))
169
+ it('should teardown the sketchViewModel listeners', sketchViewModel.assertRemoveCalls(['update', 'create', 'undo']))
170
+ it('should teardown the eventBus listeners', eventBus.assertRemoveCalls([events.MAP_STYLE_CHANGE]))
171
+ it('should reset the Done click handler', drawDone.assertOnClickReset())
172
+ it('should reset the Cancel click handler', drawCancel.assertOnClickReset())
173
+ })
174
+ })
175
+
176
+ describe('internal methods', () => {
177
+ beforeEach(jest.clearAllMocks)
178
+
179
+ it('should return null if sketchViewModel is not set', async () => {
180
+ const response = attachEvents(buildParams({
181
+ mapProvider: { sketchViewModel: null }
182
+ }))
183
+ expect(response).toBeNull()
184
+ })
185
+
186
+ it('should call handleDone when Done is clicked', async () => {
187
+ const params = buildParams()
188
+ params.pluginState.tempFeature = 'Test Feature'
189
+ attachEvents(params)
190
+ params.buttonConfig.drawDone.onClick()
191
+ expect(params.mapProvider.sketchViewModel.cancel).toHaveBeenCalled()
192
+ expect(params.mapProvider.sketchViewModel.layer).toEqual(emptySketchLayer)
193
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: null })
194
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null } })
195
+ expect(params.eventBus.emit).toHaveBeenCalledWith('draw:done', { newFeature: 'Test Feature' })
196
+ })
197
+
198
+ it('should call handleCancel when Cancel is clicked', async () => {
199
+ const params = buildParams()
200
+ params.pluginState.tempFeature = 'Test Feature'
201
+ // params.pluginState.feature = { properties: { id: 'boundary' } }
202
+ const { drawCancel } = params.buttonConfig
203
+ const { sketchViewModel, sketchLayer } = params.mapProvider
204
+ const { eventBus } = params
205
+ attachEvents(params)
206
+ drawCancel.onClick()
207
+ expect(sketchViewModel.cancel).toHaveBeenCalled()
208
+ expect(sketchLayer.removeAll).toHaveBeenCalled()
209
+ expect(createGraphic).toHaveBeenCalled()
210
+ expect(sketchLayer.add).toHaveBeenCalled()
211
+ expect(sketchViewModel.layer).toEqual(emptySketchLayer)
212
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: null })
213
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:cancelled')
214
+ })
215
+
216
+ describe('reColour', () => {
217
+ beforeEach(() => {
218
+ createSymbol.mockReturnValue(mockSymbol)
219
+ createGraphic.mockReturnValue(newGraphicMock)
220
+ mockGraphic.symbol = null
221
+ })
222
+
223
+ it('should update polygonSymbol and graphic symbols when state is not active', async () => {
224
+ const params = buildParams()
225
+ attachEvents(params)
226
+ params.eventBus.triggerEvent(events.MAP_STYLE_CHANGE)
227
+ await Promise.resolve()
228
+ expect(params.mapProvider.sketchViewModel.polygonSymbol).toEqual(mockSymbol)
229
+ expect(createGraphic).toHaveBeenCalledWith('boundary', mockGraphic.geometry.rings, params.mapColorScheme)
230
+ expect(mockGraphic.symbol).toEqual(newGraphicMock.symbol)
231
+ expect(params.mapProvider.sketchViewModel.cancel).not.toHaveBeenCalled()
232
+ })
233
+
234
+ it('should cancel and re-enter update mode when state is active and activeGraphicId is set (edit mode)', async () => {
235
+ const params = buildParams()
236
+ params.mapProvider.sketchViewModel.state = 'active'
237
+ attachEvents(params)
238
+ params.eventBus.triggerEvent(events.MAP_STYLE_CHANGE)
239
+ expect(params.mapProvider.sketchViewModel.cancel).toHaveBeenCalled()
240
+ jest.advanceTimersByTime(50)
241
+ await Promise.resolve()
242
+ await Promise.resolve()
243
+ expect(params.mapProvider.sketchViewModel.update).toHaveBeenCalledWith(mockGraphic, {
244
+ tool: 'reshape',
245
+ toggleToolOnClick: false
246
+ })
247
+ })
248
+
249
+ it('should not cancel or re-enter update mode when isCreating (active state, no activeGraphicId)', async () => {
250
+ const params = buildParams({
251
+ pluginState: { feature: null }
252
+ })
253
+ attachEvents(params)
254
+ params.eventBus.triggerEvent(events.MAP_STYLE_CHANGE)
255
+ await Promise.resolve()
256
+ expect(params.mapProvider.sketchViewModel.cancel).not.toHaveBeenCalled()
257
+ expect(params.mapProvider.sketchViewModel.update).not.toHaveBeenCalled()
258
+ })
259
+
260
+ it('should not call sketchViewModel.update if layer is not sketchLayer', async () => {
261
+ const params = buildParams()
262
+ params.mapProvider.sketchViewModel.layer = emptySketchLayer
263
+ attachEvents(params)
264
+ params.eventBus.triggerEvent(events.MAP_STYLE_CHANGE)
265
+ await Promise.resolve()
266
+ expect(params.mapProvider.sketchViewModel.update).not.toHaveBeenCalled()
267
+ })
268
+
269
+ it('should swallow AbortError thrown by sketchViewModel.update', async () => {
270
+ const abortError = Object.assign(new Error('Aborted'), { name: 'AbortError' })
271
+ const params = buildParams()
272
+ params.mapProvider.sketchViewModel.update.mockRejectedValue(abortError)
273
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
274
+ attachEvents(params)
275
+ params.eventBus.triggerEvent(events.MAP_STYLE_CHANGE)
276
+ jest.advanceTimersByTime(50)
277
+ await Promise.resolve()
278
+ await Promise.resolve()
279
+ await Promise.resolve()
280
+ expect(consoleSpy).not.toHaveBeenCalled()
281
+ consoleSpy.mockRestore()
282
+ })
283
+
284
+ it('should log non-AbortError thrown by sketchViewModel.update', async () => {
285
+ const genericError = new Error('Something went wrong')
286
+ const params = buildParams()
287
+ params.mapProvider.sketchViewModel.state = 'active'
288
+ params.mapProvider.sketchViewModel.update.mockRejectedValue(genericError)
289
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
290
+ attachEvents(params)
291
+ params.eventBus.triggerEvent(events.MAP_STYLE_CHANGE)
292
+ jest.advanceTimersByTime(50)
293
+ await Promise.resolve()
294
+ await Promise.resolve()
295
+ await Promise.resolve()
296
+ expect(consoleSpy).toHaveBeenCalledWith('Error updating sketch:', genericError)
297
+ consoleSpy.mockRestore()
298
+ })
299
+ })
300
+ })
301
+ })
@@ -25,7 +25,7 @@ function createGraphic (id, coordinates, mapColorScheme) {
25
25
  })
26
26
  }
27
27
 
28
- function graphicToGeoJSON(graphic) {
28
+ function graphicToGeoJSON (graphic) {
29
29
  if (!graphic?.geometry) {
30
30
  throw new Error('Invalid graphic')
31
31
  }
@@ -7,8 +7,8 @@ import { addFeature } from './api/addFeature.js'
7
7
  import { deleteFeature } from './api/deleteFeature.js'
8
8
 
9
9
  const buttonSlots = {
10
- mobile: { slot: 'actions', showLabel: true },
11
- tablet: { slot: 'actions', showLabel: true },
10
+ mobile: { slot: 'actions', showLabel: true },
11
+ tablet: { slot: 'actions', showLabel: true },
12
12
  desktop: { slot: 'actions', showLabel: true }
13
13
  }
14
14
 
@@ -27,7 +27,7 @@ export const manifest = {
27
27
  hiddenWhen: ({ pluginState }) => !pluginState.mode,
28
28
  enableWhen: ({ pluginState }) => !!pluginState.tempFeature,
29
29
  ...buttonSlots
30
- },{
30
+ }, {
31
31
  id: 'drawCancel',
32
32
  label: 'Cancel',
33
33
  variant: 'tertiary',
@@ -48,4 +48,4 @@ export const manifest = {
48
48
  addFeature,
49
49
  deleteFeature
50
50
  }
51
- }
51
+ }
@@ -27,4 +27,4 @@ const actions = {
27
27
  export {
28
28
  initialState,
29
29
  actions
30
- }
30
+ }
@@ -29,4 +29,4 @@ export const createSketchViewModel = ({ mapProvider }) => {
29
29
  emptySketchLayer,
30
30
  sketchLayer
31
31
  }
32
- }
32
+ }