@defra/interactive-map 0.0.4-alpha → 0.0.5-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 (202) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/index.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api.md +8 -0
  7. package/package.json +3 -2
  8. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +2 -1
  9. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt +1 -0
  10. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -1
  11. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js.LICENSE.txt +1 -0
  12. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  13. package/plugins/beta/datasets/src/datasets.js +55 -2
  14. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +206 -0
  15. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +38 -0
  16. package/plugins/beta/datasets/src/handleSetMapStyle.js +8 -1
  17. package/plugins/beta/datasets/src/mapLayers.js +41 -3
  18. package/plugins/beta/datasets/src/utils/bbox.js +113 -2
  19. package/plugins/beta/draw-ml/dist/css/index.css +1 -1
  20. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  21. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  22. package/plugins/beta/draw-ml/dist/umd/index.js +1 -1
  23. package/plugins/beta/draw-ml/src/api/addFeature.js +15 -3
  24. package/plugins/beta/draw-ml/src/api/editFeature.js +16 -5
  25. package/plugins/beta/draw-ml/src/api/newLine.js +32 -7
  26. package/plugins/beta/draw-ml/src/api/newPolygon.js +33 -8
  27. package/plugins/beta/draw-ml/src/defaults.js +1 -0
  28. package/plugins/beta/draw-ml/src/draw.scss +23 -0
  29. package/plugins/beta/draw-ml/src/events.js +55 -113
  30. package/plugins/beta/draw-ml/src/manifest.js +64 -38
  31. package/plugins/beta/draw-ml/src/mapboxSnap.js +16 -12
  32. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +4 -3
  33. package/plugins/beta/draw-ml/src/modes/editVertex/geometryHelpers.js +135 -0
  34. package/plugins/beta/draw-ml/src/modes/editVertex/helpers.js +2 -0
  35. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +134 -0
  36. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +133 -0
  37. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +141 -0
  38. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +121 -0
  39. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +51 -406
  40. package/plugins/beta/draw-ml/src/utils/flattenStyleProperties.js +49 -0
  41. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  42. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  43. package/plugins/beta/frame/src/Frame.jsx +1 -1
  44. package/plugins/beta/map-styles/dist/css/index.css +1 -1
  45. package/plugins/beta/map-styles/dist/esm/index.js +1 -1
  46. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  47. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  48. package/plugins/beta/map-styles/src/mapStyles.scss +4 -1
  49. package/plugins/interact/dist/css/index.css +1 -1
  50. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  51. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  52. package/plugins/interact/dist/umd/index.js +1 -1
  53. package/plugins/interact/src/InteractInit.jsx +1 -1
  54. package/plugins/interact/src/defaults.js +1 -0
  55. package/plugins/interact/src/events.js +37 -52
  56. package/plugins/interact/src/events.test.js +35 -0
  57. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  58. package/plugins/interact/src/hooks/useInteractionHandlers.js +80 -48
  59. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +64 -8
  60. package/plugins/interact/src/interact.scss +6 -0
  61. package/plugins/interact/src/reducer.js +25 -27
  62. package/plugins/interact/src/reducer.test.js +19 -6
  63. package/plugins/interact/src/utils/featureQueries.js +2 -2
  64. package/plugins/interact/src/utils/spatial.js +95 -0
  65. package/plugins/interact/src/utils/spatial.test.js +93 -0
  66. package/plugins/search/dist/css/index.css +1 -1
  67. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  68. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  69. package/plugins/search/dist/umd/index.js +1 -1
  70. package/plugins/search/src/Search.jsx +2 -2
  71. package/plugins/search/src/components/Form/Form.jsx +1 -1
  72. package/plugins/search/src/components/Suggestions/Suggestions.jsx +5 -5
  73. package/plugins/search/src/datasets.js +1 -1
  74. package/plugins/search/src/defaults.js +3 -0
  75. package/plugins/search/src/events/fetchSuggestions.js +48 -53
  76. package/plugins/search/src/events/formHandlers.js +3 -2
  77. package/plugins/search/src/events/index.js +1 -1
  78. package/plugins/search/src/events/inputHandlers.js +3 -1
  79. package/plugins/search/src/events/suggestionHandlers.js +12 -3
  80. package/plugins/search/src/search.scss +4 -1
  81. package/plugins/search/src/utils/parseOsNamesResults.js +12 -12
  82. package/providers/beta/esri/src/esriProvider.js +1 -1
  83. package/providers/beta/esri/src/index.js +1 -1
  84. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  85. package/providers/maplibre/dist/esm/index.js +1 -1
  86. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  87. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  88. package/providers/maplibre/dist/umd/index.js +1 -1
  89. package/providers/maplibre/src/defaults.js +3 -2
  90. package/providers/maplibre/src/index.js +22 -19
  91. package/providers/maplibre/src/maplibreProvider.js +13 -11
  92. package/providers/maplibre/src/utils/detectWebgl.js +1 -2
  93. package/providers/maplibre/src/utils/highlightFeatures.js +78 -67
  94. package/providers/maplibre/src/utils/labels.js +174 -120
  95. package/providers/maplibre/src/utils/maplibreFixes.js +2 -2
  96. package/providers/maplibre/src/utils/queryFeatures.js +146 -0
  97. package/providers/maplibre/src/utils/spatial.js +14 -17
  98. package/sonar-project.properties +39 -2
  99. package/src/App/components/Actions/Actions.jsx +2 -2
  100. package/src/App/components/Actions/Actions.module.scss +2 -2
  101. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +3 -1
  102. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +9 -0
  103. package/src/App/components/Logo/Logo.jsx +1 -1
  104. package/src/App/components/MapButton/MapButton.jsx +217 -41
  105. package/src/App/components/MapButton/MapButton.module.scss +32 -21
  106. package/src/App/components/MapButton/MapButton.test.jsx +128 -84
  107. package/src/App/components/Markers/Markers.jsx +1 -1
  108. package/src/App/components/Markers/Markers.module.scss +0 -5
  109. package/src/App/components/Panel/Panel.jsx +75 -45
  110. package/src/App/components/Panel/Panel.module.scss +18 -24
  111. package/src/App/components/Panel/Panel.test.jsx +18 -0
  112. package/src/App/components/PopupMenu/PopupMenu.jsx +246 -0
  113. package/src/App/components/PopupMenu/PopupMenu.module.scss +70 -0
  114. package/src/App/components/PopupMenu/PopupMenu.test.jsx +304 -0
  115. package/src/App/components/Tooltip/Tooltip.jsx +4 -2
  116. package/src/App/components/Tooltip/getTooltipPosition.js +1 -1
  117. package/src/App/components/Viewport/MapController.jsx +1 -1
  118. package/src/App/components/Viewport/MapStatus.jsx +1 -1
  119. package/src/App/components/Viewport/Viewport.jsx +2 -2
  120. package/src/App/components/Viewport/Viewport.module.scss +2 -2
  121. package/src/App/controls/keyboardActions.js +3 -2
  122. package/src/App/controls/keyboardShortcuts.js +4 -2
  123. package/src/App/hooks/useButtonStateEvaluator.js +58 -45
  124. package/src/App/hooks/useButtonStateEvaluator.test.js +36 -3
  125. package/src/App/hooks/useCrossHairAPI.js +49 -47
  126. package/src/App/hooks/useFocusVisible.js +2 -2
  127. package/src/App/hooks/useInterfaceAPI.js +16 -4
  128. package/src/App/hooks/useKeyboardHint.js +1 -1
  129. package/src/App/hooks/useKeyboardShortcuts.js +4 -2
  130. package/src/App/hooks/useMapAnnouncements.js +34 -32
  131. package/src/App/hooks/useMapEvents.js +12 -1
  132. package/src/App/hooks/useMapProviderOverrides.js +3 -3
  133. package/src/App/hooks/useMapStateSync.js +7 -1
  134. package/src/App/hooks/useMapURLSync.js +6 -1
  135. package/src/App/hooks/useMarkersAPI.js +88 -50
  136. package/src/App/hooks/useMediaQueryDispatch.test.js +48 -0
  137. package/src/App/hooks/useModalPanelBehaviour.js +46 -40
  138. package/src/App/hooks/useResizeObserver.js +2 -2
  139. package/src/App/initialiseApp.js +45 -38
  140. package/src/App/layout/Layout.jsx +6 -4
  141. package/src/App/layout/layout.module.scss +13 -17
  142. package/src/App/registry/keyboardShortcutRegistry.js +12 -2
  143. package/src/App/registry/keyboardShortcutRegistry.test.js +28 -0
  144. package/src/App/registry/mergeManifests.js +1 -1
  145. package/src/App/registry/panelRegistry.js +1 -1
  146. package/src/App/registry/panelRegistry.test.js +30 -1
  147. package/src/App/registry/pluginRegistry.js +14 -51
  148. package/src/App/registry/pluginRegistry.test.js +54 -6
  149. package/src/App/renderer/HtmlElementHost.jsx +186 -0
  150. package/src/App/renderer/HtmlElementHost.test.jsx +231 -0
  151. package/src/App/renderer/mapButtons.js +11 -8
  152. package/src/App/renderer/mapButtons.test.js +6 -4
  153. package/src/App/renderer/mapControls.js +6 -0
  154. package/src/App/renderer/mapControls.test.js +10 -2
  155. package/src/App/renderer/mapPanels.js +39 -26
  156. package/src/App/renderer/mapPanels.test.js +29 -0
  157. package/src/App/renderer/pluginWrapper.test.js +28 -0
  158. package/src/App/renderer/slotHelpers.js +60 -0
  159. package/src/App/renderer/slotHelpers.test.js +82 -0
  160. package/src/App/store/AppProvider.jsx +3 -0
  161. package/src/App/store/AppProvider.test.jsx +40 -0
  162. package/src/App/store/PluginProvider.jsx +7 -5
  163. package/src/App/store/appActionsMap.js +25 -1
  164. package/src/App/store/appActionsMap.test.js +21 -0
  165. package/src/App/store/appReducer.js +1 -0
  166. package/src/App/store/mapActionsMap.js +12 -24
  167. package/src/App/store/mapReducer.js +1 -1
  168. package/src/InteractiveMap/InteractiveMap.js +79 -6
  169. package/src/InteractiveMap/InteractiveMap.test.js +164 -2
  170. package/src/InteractiveMap/behaviourController.js +12 -2
  171. package/src/InteractiveMap/behaviourController.test.js +82 -1
  172. package/src/InteractiveMap/deviceChecker.js +1 -1
  173. package/src/InteractiveMap/deviceChecker.test.js +2 -2
  174. package/src/InteractiveMap/historyManager.js +41 -5
  175. package/src/InteractiveMap/historyManager.test.js +37 -6
  176. package/src/InteractiveMap/polyfills.js +39 -0
  177. package/src/InteractiveMap/polyfills.test.js +127 -0
  178. package/src/config/appConfig.js +5 -5
  179. package/src/config/appConfig.test.js +107 -42
  180. package/src/config/defaults.js +1 -0
  181. package/src/index.umd.js +1 -1
  182. package/src/scss/main.scss +1 -0
  183. package/src/scss/settings/_dimensions.scss +6 -2
  184. package/src/services/eventBus.test.js +24 -1
  185. package/src/test-utils.js +1 -0
  186. package/src/types.js +5 -0
  187. package/src/utils/detectBreakpoint.js +78 -77
  188. package/src/utils/detectBreakpoint.test.js +50 -4
  189. package/src/utils/detectInterfaceType.js +4 -4
  190. package/src/utils/getIsFullscreen.js +3 -1
  191. package/src/utils/getSafeZoneInset.js +23 -7
  192. package/src/utils/getSafeZoneInset.test.js +18 -0
  193. package/src/utils/mapStateSync.js +3 -3
  194. package/src/utils/queryString.js +1 -1
  195. package/src/utils/stringToKebab.js +1 -1
  196. package/src/utils/toggleInertElements.js +37 -12
  197. package/webpack.dev.mjs +3 -4
  198. package/plugins/interact/src/utils/turfHelpers.js +0 -30
  199. package/plugins/interact/src/utils/turfHelpers.test.js +0 -30
  200. package/plugins/search/src/utils/fetchDataset.js +0 -23
  201. package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +0 -1
  202. package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +0 -1
@@ -24,51 +24,74 @@ export const manifest = {
24
24
  InitComponent: DrawInit,
25
25
 
26
26
  buttons: [{
27
- id: 'drawDone',
28
- label: ({ pluginState }) => pluginState.action ? pluginState.action.charAt(0).toUpperCase() + pluginState.action.slice(1) : 'Done',
29
- hiddenWhen: ({ appState, pluginState }) => !pluginState.mode || appState.interfaceType !== 'mouse' && pluginState.mode !== 'edit_vertex',
30
- enableWhen: ({ pluginState }) => pluginState.action ? pluginState.actionValid : !!pluginState.tempFeature,
31
- ...createButtonSlots(true),
32
- variant: 'primary',
33
- },{
34
27
  id: 'drawAddPoint',
35
28
  label: 'Add point',
36
29
  variant: 'primary',
37
30
  hiddenWhen: ({ appState, pluginState }) => !['draw_polygon', 'draw_line'].includes(pluginState.mode) || appState.interfaceType === 'mouse',
38
31
  ...createButtonSlots(true)
39
32
  },{
40
- id: 'drawUndo',
41
- label: 'Undo',
42
- iconId: 'undo',
43
- variant: 'tertiary',
33
+ id: 'drawDone',
34
+ label: 'Done',
35
+ variant: 'primary',
44
36
  hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
45
- enableWhen: ({ pluginState }) => pluginState.undoStackLength > 0,
46
- ...createButtonSlots(false)
47
- },{
48
- id: 'drawFinish',
49
- label: 'Finish shape',
50
- iconId: 'check',
51
- variant: 'tertiary',
52
- hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line'].includes(pluginState.mode),
53
37
  enableWhen: ({ pluginState }) => pluginState.numVertecies >= (pluginState.mode === 'draw_polygon' ? 3 : 2),
54
- ...createButtonSlots(false)
55
- },{
56
- id: 'drawDeletePoint',
57
- label: 'Delete point',
58
- iconId: 'close',
59
- variant: 'tertiary',
60
- enableWhen: ({ pluginState }) => pluginState.selectedVertexIndex >= 0 && pluginState.numVertecies > (pluginState.tempFeature?.geometry?.type === 'Polygon' ? 3 : 2),
61
- hiddenWhen: ({ pluginState }) => !(['simple_select', 'edit_vertex'].includes(pluginState.mode)),
62
- ...createButtonSlots(false)
38
+ ...createButtonSlots(true)
63
39
  },{
64
- id: 'drawSnap',
65
- label: 'Snap to line',
66
- iconId: 'magnet',
40
+ id: 'drawMenu',
41
+ label: 'Menu',
42
+ iconId: 'menu',
67
43
  variant: 'tertiary',
68
- hiddenWhen: ({ pluginState }) => !pluginState.mode || !pluginState.hasSnapLayers,
69
- pressedWhen: ({ pluginState }) => !!pluginState.snap,
70
- ...createButtonSlots(false)
71
- },{
44
+ hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
45
+ menuItems: [{
46
+ id: 'drawUndo',
47
+ label: 'Undo',
48
+ iconId: 'undo',
49
+ hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
50
+ enableWhen: ({ pluginState }) => pluginState.undoStackLength > 0,
51
+ },{
52
+ id: 'drawSnap',
53
+ label: 'Snap to feature',
54
+ iconId: 'magnet',
55
+ hiddenWhen: ({ pluginState }) => !pluginState.mode || !pluginState.hasSnapLayers,
56
+ pressedWhen: ({ pluginState }) => !!pluginState.snap,
57
+ },{
58
+ id: 'drawDeletePoint',
59
+ label: 'Delete point',
60
+ iconId: 'trash',
61
+ enableWhen: ({ pluginState }) => pluginState.selectedVertexIndex >= 0 && pluginState.numVertecies > (pluginState.tempFeature?.geometry?.type === 'Polygon' ? 3 : 2),
62
+ hiddenWhen: ({ pluginState }) => !(['simple_select', 'edit_vertex'].includes(pluginState.mode)),
63
+ }],
64
+ ...createButtonSlots(true)
65
+ },
66
+ // {
67
+ // id: 'drawUndo',
68
+ // label: 'Undo',
69
+ // iconId: 'undo',
70
+ // variant: 'tertiary',
71
+ // hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
72
+ // enableWhen: ({ pluginState }) => pluginState.undoStackLength > 0,
73
+ // excludeWhen: () => true,
74
+ // ...createButtonSlots(false)
75
+ // },{
76
+ // id: 'drawDeletePoint',
77
+ // label: 'Delete point',
78
+ // iconId: 'trash',
79
+ // variant: 'tertiary',
80
+ // enableWhen: ({ pluginState }) => pluginState.selectedVertexIndex >= 0 && pluginState.numVertecies > (pluginState.tempFeature?.geometry?.type === 'Polygon' ? 3 : 2),
81
+ // hiddenWhen: ({ pluginState }) => !(['simple_select', 'edit_vertex'].includes(pluginState.mode)),
82
+ // excludeWhen: () => true,
83
+ // ...createButtonSlots(false)
84
+ // },{
85
+ // id: 'drawSnap',
86
+ // label: 'Snap to line',
87
+ // iconId: 'magnet',
88
+ // variant: 'tertiary',
89
+ // hiddenWhen: ({ pluginState }) => !pluginState.mode || !pluginState.hasSnapLayers,
90
+ // pressedWhen: ({ pluginState }) => !!pluginState.snap,
91
+ // excludeWhen: () => true,
92
+ // ...createButtonSlots(false)
93
+ // },
94
+ {
72
95
  id: 'drawCancel',
73
96
  label: 'Cancel',
74
97
  variant: 'tertiary',
@@ -84,14 +107,17 @@ export const manifest = {
84
107
  }],
85
108
 
86
109
  icons: [{
87
- id: 'check',
88
- svgContent: '<path d="M20 6 9 17l-5-5"/>'
110
+ id: 'menu',
111
+ svgContent: '<circle cx="12" cy="12" r="10"/><path d="m16 10-4 4-4-4"/>'
89
112
  },{
90
113
  id: 'undo',
91
- svgContent: '<path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/>'
114
+ svgContent: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/>'
92
115
  },{
93
116
  id: 'magnet',
94
117
  svgContent: '<path d="m12 15 4 4"/><path d="M2.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.029-6.029a1 1 0 1 1 3 3l-6.029 6.029a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.365-6.367A1 1 0 0 0 8.716 4.282z"/><path d="m5 8 4 4"/>'
118
+ },{
119
+ id: 'trash',
120
+ svgContent: '<path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>'
95
121
  }],
96
122
 
97
123
  api: {
@@ -112,9 +112,9 @@ function applyMapboxSnapPatches(colors) {
112
112
  return r
113
113
  }
114
114
 
115
- // Skip when disabled, clean up internal arrays to prevent memory accumulation
115
+ // Skip when disabled or zooming, clean up internal arrays to prevent memory accumulation
116
116
  proto.snapToClosestPoint = function(e) {
117
- if (!this.status) {
117
+ if (!this.status || this.map?._isZooming) {
118
118
  return
119
119
  }
120
120
  try {
@@ -140,6 +140,8 @@ function applyMapboxSnapPatches(colors) {
140
140
  function pollUntil(checkFn, onSuccess) {
141
141
  (function poll() {
142
142
  const result = checkFn()
143
+ // null signals to stop polling, falsy continues polling
144
+ if (result === null) return
143
145
  result ? onSuccess(result) : requestAnimationFrame(poll)
144
146
  })()
145
147
  }
@@ -267,7 +269,7 @@ export function initMapLibreSnap(map, draw, snapOptions = {}) {
267
269
  // Handle style changes - re-patch source and ensure snap layer exists
268
270
  map.on('style.load', () => {
269
271
  pollUntil(
270
- () => map.getSource('mapbox-gl-draw-hot'),
272
+ () => map._removed ? null : map.getSource('mapbox-gl-draw-hot'),
271
273
  (source) => {
272
274
  patchSourceData(source)
273
275
 
@@ -295,24 +297,26 @@ export function initMapLibreSnap(map, draw, snapOptions = {}) {
295
297
  )
296
298
  })
297
299
 
298
- // Hide snap indicator during zoom
300
+ // Suppress snap processing during zoom (indicator freezes in place)
299
301
  map.on('zoomstart', () => {
300
- if (map.getLayer(SNAP_HELPER_LAYER)) {
301
- map.setLayoutProperty(SNAP_HELPER_LAYER, 'visibility', 'none')
302
- }
302
+ map._isZooming = true
303
303
  })
304
304
 
305
305
  map.on('zoomend', () => {
306
- // Only show indicator if snap is enabled
307
- const snap = map._snapInstance
308
- if (map.getLayer(SNAP_HELPER_LAYER) && snap?.status) {
309
- map.setLayoutProperty(SNAP_HELPER_LAYER, 'visibility', 'visible')
306
+ map._isZooming = false
307
+ // Force hide then re-show to reset indicator at new zoom level (Safari fix)
308
+ if (map.getLayer(SNAP_HELPER_LAYER)) {
309
+ map.setLayoutProperty(SNAP_HELPER_LAYER, 'visibility', 'none')
310
+ const snap = map._snapInstance
311
+ if (snap?.status) {
312
+ map.setLayoutProperty(SNAP_HELPER_LAYER, 'visibility', 'visible')
313
+ }
310
314
  }
311
315
  })
312
316
 
313
317
  // Initial setup - poll until draw source exists
314
318
  pollUntil(
315
- () => map.getSource('mapbox-gl-draw-hot'),
319
+ () => map._removed ? null : map.getSource('mapbox-gl-draw-hot'),
316
320
  createSnap
317
321
  )
318
322
  }
@@ -226,7 +226,8 @@ export const createDrawMode = (ParentMode, config) => {
226
226
  interfaceType: state.interfaceType,
227
227
  vertexMarkerId: state.vertexMarkerId,
228
228
  addVertexButtonId: state.addVertexButtonId,
229
- getSnapEnabled: state.getSnapEnabled
229
+ getSnapEnabled: state.getSnapEnabled,
230
+ properties: state.properties
230
231
  })
231
232
  return true
232
233
  }
@@ -236,7 +237,7 @@ export const createDrawMode = (ParentMode, config) => {
236
237
  const initialCoords = [[center.lng, center.lat], [center.lng, center.lat]]
237
238
  const newFeature = this.newFeature({
238
239
  type: 'Feature',
239
- properties: {},
240
+ properties: state.properties || {},
240
241
  geometry: {
241
242
  type: geometryType,
242
243
  coordinates: [initialCoords]
@@ -343,7 +344,7 @@ export const createDrawMode = (ParentMode, config) => {
343
344
  const feature = e.features[0]
344
345
  draw.delete(feature.id)
345
346
  feature.id = state.featureId
346
- draw.add(feature)
347
+ draw.add(feature, { userProperties: true })
347
348
  },
348
349
 
349
350
  onVertexButtonClick(state, e) {
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Pure geometry helper functions for multi-ring/multi-part geometry support.
3
+ * These functions handle coordinate transformations between flat arrays and
4
+ * hierarchical GeoJSON structures for Polygon, MultiPolygon, LineString, and MultiLineString.
5
+ */
6
+
7
+ /**
8
+ * Get flat coordinates array from feature for all geometry types.
9
+ * Flattens all rings/parts into a single array for unified vertex navigation.
10
+ *
11
+ * @param {Object} feature - GeoJSON geometry object
12
+ * @returns {Array<[number, number]>} Flat array of all coordinates
13
+ */
14
+ export const getCoords = (feature) => {
15
+ if (!feature?.coordinates) return []
16
+ switch (feature.type) {
17
+ case 'LineString':
18
+ return feature.coordinates
19
+ case 'Polygon':
20
+ return feature.coordinates.flat(1)
21
+ case 'MultiLineString':
22
+ return feature.coordinates.flat(1)
23
+ case 'MultiPolygon':
24
+ return feature.coordinates.flat(2)
25
+ default:
26
+ return []
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get segment metadata for multi-ring/multi-part geometries.
32
+ * Each segment represents a ring (for Polygon) or part (for Multi*).
33
+ *
34
+ * @param {Object} feature - GeoJSON geometry object
35
+ * @returns {Array<{start: number, length: number, path: number[], closed: boolean}>}
36
+ * Array of segment metadata objects where:
37
+ * - start: Starting index in flat coordinates array
38
+ * - length: Number of coordinates in this segment
39
+ * - path: Hierarchical path indices to reach this segment in GeoJSON
40
+ * - closed: Whether this segment is closed (true for Polygon rings)
41
+ */
42
+ export const getRingSegments = (feature) => {
43
+ if (!feature?.coordinates) return []
44
+ const segments = []
45
+ let start = 0
46
+
47
+ switch (feature.type) {
48
+ case 'LineString':
49
+ segments.push({ start: 0, length: feature.coordinates.length, path: [], closed: false })
50
+ break
51
+ case 'Polygon':
52
+ feature.coordinates.forEach((ring, ringIdx) => {
53
+ segments.push({ start, length: ring.length, path: [ringIdx], closed: true })
54
+ start += ring.length
55
+ })
56
+ break
57
+ case 'MultiLineString':
58
+ feature.coordinates.forEach((line, lineIdx) => {
59
+ segments.push({ start, length: line.length, path: [lineIdx], closed: false })
60
+ start += line.length
61
+ })
62
+ break
63
+ case 'MultiPolygon':
64
+ feature.coordinates.forEach((polygon, polyIdx) => {
65
+ polygon.forEach((ring, ringIdx) => {
66
+ segments.push({ start, length: ring.length, path: [polyIdx, ringIdx], closed: true })
67
+ start += ring.length
68
+ })
69
+ })
70
+ break
71
+ default:
72
+ break
73
+ }
74
+
75
+ return segments
76
+ }
77
+
78
+ /**
79
+ * Find which segment a flat vertex index belongs to.
80
+ *
81
+ * @param {Array} segments - Array of segment metadata from getRingSegments
82
+ * @param {number} flatIdx - Flat vertex index
83
+ * @returns {{segment: Object, localIdx: number}|null}
84
+ * Object with segment metadata and local index within that segment, or null if not found
85
+ */
86
+ export const getSegmentForIndex = (segments, flatIdx) => {
87
+ for (const seg of segments) {
88
+ if (flatIdx >= seg.start && flatIdx < seg.start + seg.length) {
89
+ return { segment: seg, localIdx: flatIdx - seg.start }
90
+ }
91
+ }
92
+ return null
93
+ }
94
+
95
+ /**
96
+ * Get modifiable coordinate array at a specific hierarchical path.
97
+ * Returns a reference to the actual coordinate array in the GeoJSON structure.
98
+ *
99
+ * @param {Object} geojson - Full GeoJSON feature object
100
+ * @param {number[]} path - Hierarchical path indices (e.g., [0] for first ring, [1, 0] for second polygon's first ring)
101
+ * @returns {Array<[number, number]>} Reference to coordinate array at path
102
+ */
103
+ export const getModifiableCoords = (geojson, path) => {
104
+ let coords = geojson.geometry.coordinates
105
+ for (const idx of path) {
106
+ coords = coords[idx]
107
+ }
108
+ return coords
109
+ }
110
+
111
+ /**
112
+ * Convert mapbox-gl-draw coord_path string to flat vertex index.
113
+ * coord_path format: "ringIdx.vertexIdx" for Polygon, "polyIdx.ringIdx.vertexIdx" for MultiPolygon, etc.
114
+ *
115
+ * @param {Object} feature - GeoJSON geometry object
116
+ * @param {string} coordPath - coord_path string from mapbox-gl-draw
117
+ * @returns {number} Flat vertex index in the unified coordinate array
118
+ */
119
+ export const coordPathToFlatIndex = (feature, coordPath) => {
120
+ const parts = coordPath.split('.').map(Number)
121
+ const segments = getRingSegments(feature)
122
+
123
+ // Match coord_path to segment
124
+ for (const seg of segments) {
125
+ // Check if path matches (compare all but last element which is the local vertex index)
126
+ const pathMatches = seg.path.every((val, idx) => val === parts[idx])
127
+ if (pathMatches && parts.length === seg.path.length + 1) {
128
+ const localIdx = parts[parts.length - 1]
129
+ return seg.start + localIdx
130
+ }
131
+ }
132
+
133
+ // Fallback: just use the last number (works for simple geometries)
134
+ return parts[parts.length - 1]
135
+ }
@@ -0,0 +1,2 @@
1
+ export const scalePoint = (point, scale) => ({ x: point.x * scale, y: point.y * scale })
2
+ export const isOnSVG = (el) => el instanceof window.SVGElement || el.ownerSVGElement
@@ -0,0 +1,134 @@
1
+ import {
2
+ getSnapInstance, isSnapEnabled, triggerSnapAtPoint, getSnapLngLat,
3
+ clearSnapState, clearSnapIndicator
4
+ } from '../../utils/snapHelpers.js'
5
+ import { coordPathToFlatIndex } from './geometryHelpers.js'
6
+ import { isOnSVG } from './helpers.js'
7
+
8
+ const touchVertexTarget = `
9
+ <svg width='48' height='48' viewBox='0 0 48 48' fill-rule='evenodd' style='display:none;position:absolute;top:50%;left:50%;margin:24px 0 0 -24px' class='touch-vertex-target' data-touch-vertex-target>
10
+ <circle cx='24' cy='24' r='24' fill='currentColor'/>
11
+ <path d="M37.543 25H34a1 1 0 1 1 0-2h3.629l-.836-.837a1 1 0 0 1 1.414-1.414l2.5 2.501A1 1 0 0 1 41 24a1 1 0 0 1-.487.858l-2.306 2.306a1 1 0 0 1-1.414-1.414l.75-.75zM23 10.414l-.793.793a1 1 0 0 1-1.414-1.414l2.5-2.5C23.481 7.105 23.734 7 24 7s.519.105.707.293l2.5 2.5a1 1 0 0 1-1.414 1.414L25 10.414V14a1 1 0 1 1-2 0v-3.586zM7 24a1 1 0 0 1 .293-.75l2.5-2.501a1 1 0 0 1 1.414 1.414l-.836.837H14a1 1 0 1 1 0 2h-3.543l.75.75a1 1 0 0 1-1.414 1.414l-2.306-2.306A1 1 0 0 1 7 24zm16.293 16.707l-2.5-2.5a1 1 0 0 1 1.414-1.414l.793.793V34a1 1 0 1 1 2 0v3.586l.793-.793a1 1 0 0 1 1.414 1.414l-2.5 2.5c-.188.188-.441.293-.707.293s-.519-.105-.707-.293zM24 20c2.208 0 4 1.792 4 4s-1.792 4-4 4-4-1.792-4-4 1.792-4 4-4z" fill="#fff"/>
12
+ </svg>
13
+ `
14
+
15
+ export const touchHandlers = {
16
+ addTouchVertexTarget(state) {
17
+ let el = state.container.querySelector('[data-touch-vertex-target]')
18
+ if (!el) {
19
+ state.container.insertAdjacentHTML('beforeend', touchVertexTarget)
20
+ el = state.container.querySelector('[data-touch-vertex-target]')
21
+ }
22
+ state.touchVertexTarget = el
23
+ },
24
+
25
+ updateTouchVertexTarget(state, point) {
26
+ if (point && state.interfaceType === 'touch' && state.selectedVertexIndex >= 0) {
27
+ Object.assign(state.touchVertexTarget.style, { display: 'block', top: `${point.y}px`, left: `${point.x}px` })
28
+ } else {
29
+ state.touchVertexTarget.style.display = 'none'
30
+ }
31
+ },
32
+
33
+ hideTouchVertexIndicator(state) {
34
+ state.touchVertexTarget.style.display = 'none'
35
+ },
36
+
37
+ onPointerevent(state, e) {
38
+ state.interfaceType = e.pointerType === 'touch' ? 'touch' : 'pointer'
39
+ state.isPanEnabled = true
40
+ if (e.pointerType === 'touch' && e.type === 'pointermove' && !isOnSVG(e.target.parentNode) && !state._ignorePointermoveDeselect) {
41
+ this.changeMode(state, { selectedVertexIndex: -1, selectedVertexType: null, coordPath: null })
42
+ }
43
+ },
44
+
45
+ // Empty stubs required by DirectSelect
46
+ onTouchStart() {},
47
+ onTouchMove() {},
48
+ onTouchEnd() {},
49
+
50
+ onTouchend(state) {
51
+ clearSnapState(getSnapInstance(this.map))
52
+ if (state?.featureId) {
53
+ this.syncVertices(state)
54
+
55
+ // Push undo for the move if touch actually moved
56
+ if (state._touchMoved && state._moveStartPosition && state._moveStartIndex !== undefined) {
57
+ this.pushUndo({
58
+ type: 'move_vertex',
59
+ featureId: state.featureId,
60
+ vertexIndex: state._moveStartIndex,
61
+ previousPosition: state._moveStartPosition
62
+ })
63
+ }
64
+ state._moveStartPosition = null
65
+ state._moveStartIndex = undefined
66
+ state._touchMoved = false
67
+ }
68
+ },
69
+
70
+ onTap(state, e) {
71
+ // Hide snap indicator on any tap
72
+ const snap = getSnapInstance(this.map)
73
+ if (snap) {
74
+ clearSnapIndicator(snap, this.map)
75
+ }
76
+
77
+ const meta = e.featureTarget?.properties.meta
78
+ const coordPath = e.featureTarget?.properties.coord_path
79
+
80
+ if (meta === 'vertex') {
81
+ const feature = this.getFeature(state.featureId)
82
+ const idx = coordPathToFlatIndex(feature, coordPath)
83
+ this.changeMode(state, {
84
+ selectedVertexIndex: idx,
85
+ selectedVertexType: 'vertex',
86
+ coordPath
87
+ })
88
+ } else if (meta === 'midpoint') {
89
+ this.insertVertex({ ...state, selectedVertexIndex: this.getVertexIndexFromMidpoint(state, coordPath), selectedVertexType: 'midpoint' })
90
+ } else {
91
+ this.clickNoTarget(state)
92
+ }
93
+ },
94
+
95
+ onTouchstart(state, e) {
96
+ clearSnapState(getSnapInstance(this.map))
97
+ const vertex = state.vertecies?.[state.selectedVertexIndex]
98
+ if (!vertex || !isOnSVG(e.target.parentNode)) {
99
+ return
100
+ }
101
+
102
+ // Save starting position for undo
103
+ state._moveStartPosition = [...vertex]
104
+ state._moveStartIndex = state.selectedVertexIndex
105
+ state._touchMoved = false
106
+
107
+ const touch = { x: e.touches[0].clientX, y: e.touches[0].clientY }
108
+ const style = window.getComputedStyle(state.touchVertexTarget)
109
+ state.deltaTarget = { x: touch.x - Number.parseFloat(style.left), y: touch.y - Number.parseFloat(style.top) }
110
+ const vertexPt = this.map.project(vertex)
111
+ state.deltaVertex = { x: (touch.x / state.scale) - vertexPt.x, y: (touch.y / state.scale) - vertexPt.y }
112
+ },
113
+
114
+ onTouchmove(state, e) {
115
+ if (state.selectedVertexIndex < 0 || !isOnSVG(e.target.parentNode)) {
116
+ return
117
+ }
118
+
119
+ state._touchMoved = true
120
+
121
+ const touch = { x: e.touches[0].clientX, y: e.touches[0].clientY }
122
+ const screenPt = { x: (touch.x / state.scale) - state.deltaVertex.x, y: (touch.y / state.scale) - state.deltaVertex.y }
123
+
124
+ let finalCoord = this.map.unproject(screenPt)
125
+ if (isSnapEnabled(state)) {
126
+ const snap = getSnapInstance(this.map)
127
+ triggerSnapAtPoint(snap, this.map, screenPt)
128
+ finalCoord = getSnapLngLat(snap) || finalCoord
129
+ }
130
+
131
+ this.moveVertex(state, finalCoord)
132
+ this.updateTouchVertexTarget(state, { x: touch.x - state.deltaTarget.x, y: touch.y - state.deltaTarget.y })
133
+ }
134
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ getRingSegments,
3
+ getSegmentForIndex,
4
+ getModifiableCoords
5
+ } from './geometryHelpers.js'
6
+ import { scalePoint } from './helpers.js'
7
+
8
+ export const undoHandlers = {
9
+ // Fire geometry change event (for external listeners)
10
+ fireGeometryChange(state) {
11
+ const feature = this.getFeature(state.featureId)
12
+ if (feature) {
13
+ this.map.fire('draw.update', {
14
+ features: [feature.toGeoJSON()],
15
+ action: 'change_coordinates'
16
+ })
17
+ }
18
+ },
19
+
20
+ // Undo support
21
+ pushUndo(operation) {
22
+ const undoStack = this.map._undoStack
23
+ if (!undoStack) {
24
+ return
25
+ }
26
+ undoStack.push(operation)
27
+ },
28
+
29
+ handleUndo(state) {
30
+ const undoStack = this.map._undoStack
31
+ if (!undoStack || undoStack.length === 0) {
32
+ return
33
+ }
34
+
35
+ const op = undoStack.pop()
36
+
37
+ if (op.type === 'move_vertex') {
38
+ this.undoMoveVertex(state, op)
39
+ } else if (op.type === 'insert_vertex') {
40
+ this.undoInsertVertex(state, op)
41
+ } else if (op.type === 'delete_vertex') {
42
+ this.undoDeleteVertex(state, op)
43
+ }
44
+ },
45
+
46
+ undoMoveVertex(state, op) {
47
+ const { vertexIndex, previousPosition, featureId } = op
48
+ const feature = this.getFeature(featureId)
49
+ if (!feature) return
50
+
51
+ const geojson = feature.toGeoJSON()
52
+ const segments = getRingSegments(feature)
53
+ const result = getSegmentForIndex(segments, vertexIndex)
54
+ if (!result) return
55
+
56
+ const coords = getModifiableCoords(geojson, result.segment.path)
57
+ coords[result.localIdx] = previousPosition
58
+ this._applyUndoAndSync(state, geojson, featureId)
59
+
60
+ // Update touch vertex target position
61
+ const vertex = state.vertecies[state.selectedVertexIndex]
62
+ if (vertex) {
63
+ this.updateTouchVertexTarget(state, scalePoint(this.map.project(vertex), state.scale))
64
+ }
65
+ },
66
+
67
+ undoInsertVertex(state, op) {
68
+ const { vertexIndex, featureId } = op
69
+ const feature = this.getFeature(featureId)
70
+ if (!feature) return
71
+
72
+ const geojson = feature.toGeoJSON()
73
+ const segments = getRingSegments(feature)
74
+ const result = getSegmentForIndex(segments, vertexIndex)
75
+ if (!result) return
76
+
77
+ const coords = getModifiableCoords(geojson, result.segment.path)
78
+ coords.splice(result.localIdx, 1)
79
+ this._applyUndoAndSync(state, geojson, featureId)
80
+
81
+ // Clear DirectSelect's coordinate selection
82
+ this.clearSelectedCoordinates()
83
+ this.hideTouchVertexIndicator(state)
84
+ this.changeMode(state, { selectedVertexIndex: -1, selectedVertexType: null })
85
+ },
86
+
87
+ undoDeleteVertex(state, op) {
88
+ const { vertexIndex, position, featureId } = op
89
+ const feature = this.getFeature(featureId)
90
+ if (!feature) {
91
+ return
92
+ }
93
+
94
+ const geojson = feature.toGeoJSON()
95
+ const segments = getRingSegments(feature)
96
+
97
+ // Try to find segment containing vertexIndex
98
+ let result = getSegmentForIndex(segments, vertexIndex)
99
+
100
+ // If not found, vertex might be at segment boundary
101
+ if (!result) {
102
+ for (const seg of segments) {
103
+ if (vertexIndex === seg.start + seg.length) {
104
+ result = { segment: seg, localIdx: seg.length }
105
+ break
106
+ }
107
+ }
108
+ }
109
+
110
+ if (!result) {
111
+ return
112
+ }
113
+
114
+ const coords = getModifiableCoords(geojson, result.segment.path)
115
+ coords.splice(result.localIdx, 0, position)
116
+ this._applyUndoAndSync(state, geojson, featureId)
117
+
118
+ // Update touch vertex target to restored vertex position
119
+ const vertex = state.vertecies[vertexIndex]
120
+ if (vertex) {
121
+ this.updateTouchVertexTarget(state, scalePoint(this.map.project(vertex), state.scale))
122
+ }
123
+ this.changeMode(state, { selectedVertexIndex: vertexIndex, selectedVertexType: 'vertex', coordPath: this.getCoordPath(state, vertexIndex) })
124
+ },
125
+
126
+ _applyUndoAndSync(state, geojson, featureId) {
127
+ this._ctx.api.add(geojson)
128
+ state.vertecies = this.getVerticies(featureId)
129
+ state.midpoints = this.getMidpoints(featureId)
130
+ this._ctx.store.render()
131
+ this.fireGeometryChange(state)
132
+ }
133
+ }