@defra/interactive-map 0.0.3-alpha → 0.0.4-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 (268) hide show
  1. package/README.md +16 -57
  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/button-definition.md +176 -0
  7. package/docs/api/context.md +124 -0
  8. package/docs/api/control-definition.md +72 -0
  9. package/docs/api/icon-definition.md +28 -0
  10. package/docs/api/map-style-config.md +76 -0
  11. package/docs/api/marker-config.md +51 -0
  12. package/docs/api/panel-definition.md +117 -0
  13. package/docs/api/slots.md +25 -0
  14. package/docs/api.md +713 -0
  15. package/docs/architecture.md +139 -0
  16. package/docs/building-a-plugin.md +149 -0
  17. package/docs/getting-started.md +46 -0
  18. package/docs/govuk-prototype.md +0 -0
  19. package/docs/plugins/interact.md +205 -0
  20. package/docs/plugins/map-styles.md +84 -0
  21. package/docs/plugins/plugin-context.md +67 -0
  22. package/docs/plugins/plugin-descriptor.md +71 -0
  23. package/docs/plugins/plugin-manifest.md +118 -0
  24. package/docs/plugins/scale-bar.md +62 -0
  25. package/docs/plugins/search.md +162 -0
  26. package/docs/plugins.md +116 -0
  27. package/govuk-prototype-kit.config.json +7 -7
  28. package/package.json +14 -1
  29. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -0
  30. package/plugins/beta/datasets/dist/esm/index.js +2 -0
  31. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -0
  32. package/plugins/beta/datasets/dist/umd/index.js +2 -0
  33. package/plugins/{datasets → beta/datasets}/src/manifest.js +1 -1
  34. package/plugins/{datasets → beta/datasets}/src/mapLayers.js +1 -1
  35. package/plugins/{datasets → beta/datasets}/src/panels/Key.jsx +1 -1
  36. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -0
  37. package/plugins/beta/draw-ml/dist/esm/index.js +2 -0
  38. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -0
  39. package/plugins/beta/draw-ml/dist/umd/index.js +2 -0
  40. package/plugins/{draw-ml → beta/draw-ml}/src/api/editFeature.js +1 -1
  41. package/plugins/beta/draw-ml/src/api/merge.js +9 -0
  42. package/plugins/beta/draw-ml/src/api/newLine.js +43 -0
  43. package/plugins/{draw-ml → beta/draw-ml}/src/api/newPolygon.js +5 -5
  44. package/plugins/beta/draw-ml/src/api/split.js +81 -0
  45. package/plugins/{draw-ml → beta/draw-ml}/src/defaults.js +2 -0
  46. package/plugins/{draw-ml → beta/draw-ml}/src/events.js +105 -3
  47. package/plugins/{draw-ml → beta/draw-ml}/src/manifest.js +17 -11
  48. package/plugins/{draw-ml → beta/draw-ml}/src/mapboxDraw.js +51 -2
  49. package/plugins/{draw-ml → beta/draw-ml}/src/mapboxSnap.js +2 -0
  50. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +496 -0
  51. package/plugins/beta/draw-ml/src/modes/drawLineMode.js +18 -0
  52. package/plugins/beta/draw-ml/src/modes/drawPolygonMode.js +16 -0
  53. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +792 -0
  54. package/plugins/{draw-ml → beta/draw-ml}/src/reducer.js +30 -2
  55. package/plugins/{draw-ml → beta/draw-ml}/src/styles.js +66 -36
  56. package/plugins/beta/draw-ml/src/undoStack.js +50 -0
  57. package/plugins/beta/draw-ml/src/utils/debounce.js +16 -0
  58. package/plugins/beta/draw-ml/src/utils/spatial.js +258 -0
  59. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -0
  60. package/plugins/beta/frame/dist/esm/index.js +2 -0
  61. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -0
  62. package/plugins/beta/frame/dist/umd/index.js +2 -0
  63. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -0
  64. package/plugins/beta/map-styles/dist/esm/index.js +2 -0
  65. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -0
  66. package/plugins/beta/map-styles/dist/umd/index.js +2 -0
  67. package/plugins/{map-styles → beta/map-styles}/src/MapStyles.jsx +1 -1
  68. package/plugins/{map-styles → beta/map-styles}/src/manifest.js +8 -10
  69. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -0
  70. package/plugins/beta/scale-bar/dist/esm/index.js +2 -0
  71. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -0
  72. package/plugins/beta/scale-bar/dist/umd/index.js +2 -0
  73. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -0
  74. package/plugins/beta/use-location/dist/esm/index.js +2 -0
  75. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -0
  76. package/plugins/beta/use-location/dist/umd/index.js +2 -0
  77. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  78. package/plugins/interact/dist/esm/index.js +1 -1
  79. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  80. package/plugins/interact/dist/umd/index.js +1 -1
  81. package/plugins/interact/src/InteractInit.test.js +97 -0
  82. package/plugins/interact/src/api/clear.test.js +18 -0
  83. package/plugins/interact/src/api/disable.test.js +12 -0
  84. package/plugins/interact/src/api/enable.test.js +48 -0
  85. package/plugins/interact/src/api/selectFeature.test.js +33 -0
  86. package/plugins/interact/src/api/unselectFeature.test.js +33 -0
  87. package/plugins/interact/src/defaults.js +1 -0
  88. package/plugins/interact/src/events.test.js +151 -0
  89. package/plugins/interact/src/hooks/useHighlightSync.test.js +212 -0
  90. package/plugins/interact/src/hooks/useInteractionHandlers.js +14 -7
  91. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +260 -0
  92. package/plugins/interact/src/index.test.js +22 -0
  93. package/plugins/interact/src/manifest.js +1 -1
  94. package/plugins/interact/src/manifest.test.js +94 -0
  95. package/plugins/interact/src/reducer.js +10 -1
  96. package/plugins/interact/src/reducer.test.js +142 -0
  97. package/plugins/interact/src/utils/buildStylesMap.test.js +57 -0
  98. package/plugins/interact/src/utils/featureQueries.test.js +56 -0
  99. package/plugins/interact/src/utils/turfHelpers.js +30 -0
  100. package/plugins/interact/src/utils/turfHelpers.test.js +30 -0
  101. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  102. package/plugins/search/dist/esm/index.js +1 -1
  103. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  104. package/plugins/search/dist/umd/index.js +1 -1
  105. package/plugins/search/src/utils/parseOsNamesResults.js +3 -7
  106. package/providers/{esri → beta/esri}/src/mapEvents.js +2 -2
  107. package/providers/beta/open-names/dist/esm/im-reverse-geocode.js +2 -0
  108. package/providers/beta/open-names/dist/esm/index.js +2 -0
  109. package/providers/beta/open-names/dist/umd/im-reverse-geocode.js +2 -0
  110. package/providers/beta/open-names/dist/umd/index.js +2 -0
  111. package/providers/maplibre/dist/esm/im-maplibre-framework.js +1 -1
  112. package/providers/maplibre/dist/esm/im-maplibre-framework.js.LICENSE.txt +1 -1
  113. package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +1 -1
  114. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  115. package/providers/maplibre/dist/esm/index.js +1 -1
  116. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  117. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  118. package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +1 -1
  119. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  120. package/providers/maplibre/dist/umd/index.js +1 -1
  121. package/providers/maplibre/src/index.js +14 -1
  122. package/providers/maplibre/src/maplibreProvider.js +127 -0
  123. package/sonar-project.properties +1 -1
  124. package/src/App/hooks/useInterfaceAPI.js +24 -1
  125. package/src/App/renderer/mapButtons.js +1 -1
  126. package/src/App/store/appActionsMap.js +22 -1
  127. package/src/InteractiveMap/InteractiveMap.js +114 -6
  128. package/src/config/defaults.js +29 -20
  129. package/src/config/events.js +110 -4
  130. package/src/index.js +5 -2
  131. package/src/scss/tools/_border-focus.scss +20 -4
  132. package/src/types.js +585 -0
  133. package/webpack.esm.mjs +8 -8
  134. package/webpack.umd.mjs +8 -8
  135. package/plugins/datasets/dist/esm/im-datasets-plugin.js +0 -1
  136. package/plugins/datasets/dist/esm/index.js +0 -2
  137. package/plugins/datasets/dist/umd/im-datasets-plugin.js +0 -1
  138. package/plugins/datasets/dist/umd/index.js +0 -2
  139. package/plugins/draw-ml/dist/esm/im-draw-ml-plugin.js +0 -1
  140. package/plugins/draw-ml/dist/esm/index.js +0 -2
  141. package/plugins/draw-ml/dist/umd/im-draw-ml-plugin.js +0 -1
  142. package/plugins/draw-ml/dist/umd/index.js +0 -2
  143. package/plugins/draw-ml/src/modes/drawVertexMode.js +0 -275
  144. package/plugins/draw-ml/src/modes/editVertexMode.js +0 -426
  145. package/plugins/draw-ml/src/utils.js +0 -102
  146. package/plugins/frame/dist/esm/im-frame-plugin.js +0 -1
  147. package/plugins/frame/dist/esm/index.js +0 -2
  148. package/plugins/frame/dist/umd/im-frame-plugin.js +0 -1
  149. package/plugins/frame/dist/umd/index.js +0 -2
  150. package/plugins/map-styles/dist/esm/im-map-styles-plugin.js +0 -1
  151. package/plugins/map-styles/dist/esm/index.js +0 -2
  152. package/plugins/map-styles/dist/umd/im-map-styles-plugin.js +0 -1
  153. package/plugins/map-styles/dist/umd/index.js +0 -2
  154. package/plugins/scale-bar/dist/esm/im-scale-bar-plugin.js +0 -1
  155. package/plugins/scale-bar/dist/esm/index.js +0 -2
  156. package/plugins/scale-bar/dist/umd/im-scale-bar-plugin.js +0 -1
  157. package/plugins/scale-bar/dist/umd/index.js +0 -2
  158. package/plugins/use-location/dist/esm/im-use-location-plugin.js +0 -1
  159. package/plugins/use-location/dist/esm/index.js +0 -2
  160. package/plugins/use-location/dist/umd/im-use-location-plugin.js +0 -1
  161. package/plugins/use-location/dist/umd/index.js +0 -2
  162. package/providers/open-names/dist/esm/im-reverse-geocode.js +0 -2
  163. package/providers/open-names/dist/esm/index.js +0 -2
  164. package/providers/open-names/dist/umd/im-reverse-geocode.js +0 -2
  165. package/providers/open-names/dist/umd/index.js +0 -2
  166. /package/docs/architecture/{ARCHITECTURE_DIAGRAMS.md → architecture-diagrams.md} +0 -0
  167. /package/plugins/{datasets → beta/datasets}/dist/css/index.css +0 -0
  168. /package/plugins/{datasets → beta/datasets}/dist/esm/index.js.LICENSE.txt +0 -0
  169. /package/plugins/{datasets → beta/datasets}/dist/umd/index.js.LICENSE.txt +0 -0
  170. /package/plugins/{datasets → beta/datasets}/src/DatasetsInit.jsx +0 -0
  171. /package/plugins/{datasets → beta/datasets}/src/api/addDataset.js +0 -0
  172. /package/plugins/{datasets → beta/datasets}/src/api/hideDataset.js +0 -0
  173. /package/plugins/{datasets → beta/datasets}/src/api/hideFeatures.js +0 -0
  174. /package/plugins/{datasets → beta/datasets}/src/api/removeDataset.js +0 -0
  175. /package/plugins/{datasets → beta/datasets}/src/api/showDataset.js +0 -0
  176. /package/plugins/{datasets → beta/datasets}/src/api/showFeatures.js +0 -0
  177. /package/plugins/{datasets → beta/datasets}/src/datasets.js +0 -0
  178. /package/plugins/{datasets → beta/datasets}/src/datasets.scss +0 -0
  179. /package/plugins/{datasets → beta/datasets}/src/defaults.js +0 -0
  180. /package/plugins/{datasets → beta/datasets}/src/handleSetMapStyle.js +0 -0
  181. /package/plugins/{datasets → beta/datasets}/src/index.js +0 -0
  182. /package/plugins/{datasets → beta/datasets}/src/panels/Key.module.scss +0 -0
  183. /package/plugins/{datasets → beta/datasets}/src/panels/Layers.jsx +0 -0
  184. /package/plugins/{datasets → beta/datasets}/src/panels/Layers.module.scss +0 -0
  185. /package/plugins/{datasets → beta/datasets}/src/reducer.js +0 -0
  186. /package/plugins/{datasets → beta/datasets}/src/utils/bbox.js +0 -0
  187. /package/plugins/{datasets → beta/datasets}/src/utils/debounce.js +0 -0
  188. /package/plugins/{datasets → beta/datasets}/src/utils/filters.js +0 -0
  189. /package/plugins/{draw-es → beta/draw-es}/src/DrawInit.jsx +0 -0
  190. /package/plugins/{draw-es → beta/draw-es}/src/api/addFeature.js +0 -0
  191. /package/plugins/{draw-es → beta/draw-es}/src/api/deleteFeature.js +0 -0
  192. /package/plugins/{draw-es → beta/draw-es}/src/api/editFeature.js +0 -0
  193. /package/plugins/{draw-es → beta/draw-es}/src/api/newPolygon.js +0 -0
  194. /package/plugins/{draw-es → beta/draw-es}/src/events.js +0 -0
  195. /package/plugins/{draw-es → beta/draw-es}/src/graphic.js +0 -0
  196. /package/plugins/{draw-es → beta/draw-es}/src/index.js +0 -0
  197. /package/plugins/{draw-es → beta/draw-es}/src/manifest.js +0 -0
  198. /package/plugins/{draw-es → beta/draw-es}/src/reducer.js +0 -0
  199. /package/plugins/{draw-es → beta/draw-es}/src/sketchViewModel.js +0 -0
  200. /package/plugins/{draw-ml → beta/draw-ml}/dist/css/index.css +0 -0
  201. /package/plugins/{draw-ml → beta/draw-ml}/dist/esm/index.js.LICENSE.txt +0 -0
  202. /package/plugins/{draw-ml → beta/draw-ml}/dist/umd/index.js.LICENSE.txt +0 -0
  203. /package/plugins/{draw-ml → beta/draw-ml}/src/DrawInit.jsx +0 -0
  204. /package/plugins/{draw-ml → beta/draw-ml}/src/api/addFeature.js +0 -0
  205. /package/plugins/{draw-ml → beta/draw-ml}/src/api/deleteFeature.js +0 -0
  206. /package/plugins/{draw-ml → beta/draw-ml}/src/draw.scss +0 -0
  207. /package/plugins/{draw-ml → beta/draw-ml}/src/index.js +0 -0
  208. /package/plugins/{draw-ml → beta/draw-ml}/src/modes/disabledMode.js +0 -0
  209. /package/plugins/{draw-ml/src → beta/draw-ml/src/utils}/snapHelpers.js +0 -0
  210. /package/plugins/{frame → beta/frame}/dist/css/index.css +0 -0
  211. /package/plugins/{frame → beta/frame}/dist/esm/index.js.LICENSE.txt +0 -0
  212. /package/plugins/{frame → beta/frame}/dist/umd/index.js.LICENSE.txt +0 -0
  213. /package/plugins/{frame → beta/frame}/package.json +0 -0
  214. /package/plugins/{frame → beta/frame}/src/Frame.jsx +0 -0
  215. /package/plugins/{frame → beta/frame}/src/FrameInit.jsx +0 -0
  216. /package/plugins/{frame → beta/frame}/src/api/addFrame.js +0 -0
  217. /package/plugins/{frame → beta/frame}/src/api/editFeature.js +0 -0
  218. /package/plugins/{frame → beta/frame}/src/config.js +0 -0
  219. /package/plugins/{frame → beta/frame}/src/frame.scss +0 -0
  220. /package/plugins/{frame → beta/frame}/src/index.js +0 -0
  221. /package/plugins/{frame → beta/frame}/src/manifest.js +0 -0
  222. /package/plugins/{frame → beta/frame}/src/reducer.js +0 -0
  223. /package/plugins/{frame → beta/frame}/src/utils.js +0 -0
  224. /package/plugins/{map-styles → beta/map-styles}/dist/css/index.css +0 -0
  225. /package/plugins/{map-styles → beta/map-styles}/dist/esm/index.js.LICENSE.txt +0 -0
  226. /package/plugins/{map-styles → beta/map-styles}/dist/umd/index.js.LICENSE.txt +0 -0
  227. /package/plugins/{map-styles → beta/map-styles}/src/MapStylesInit.jsx +0 -0
  228. /package/plugins/{map-styles → beta/map-styles}/src/config.js +0 -0
  229. /package/plugins/{map-styles → beta/map-styles}/src/index.js +0 -0
  230. /package/plugins/{map-styles → beta/map-styles}/src/mapStyles.scss +0 -0
  231. /package/plugins/{scale-bar → beta/scale-bar}/dist/css/index.css +0 -0
  232. /package/plugins/{scale-bar → beta/scale-bar}/dist/esm/index.js.LICENSE.txt +0 -0
  233. /package/plugins/{scale-bar → beta/scale-bar}/dist/umd/index.js.LICENSE.txt +0 -0
  234. /package/plugins/{scale-bar → beta/scale-bar}/package.json +0 -0
  235. /package/plugins/{scale-bar → beta/scale-bar}/src/ScaleBar.jsx +0 -0
  236. /package/plugins/{scale-bar → beta/scale-bar}/src/ScaleBar.test.jsx +0 -0
  237. /package/plugins/{scale-bar → beta/scale-bar}/src/index.js +0 -0
  238. /package/plugins/{scale-bar → beta/scale-bar}/src/index.test.js +0 -0
  239. /package/plugins/{scale-bar → beta/scale-bar}/src/manifest.js +0 -0
  240. /package/plugins/{scale-bar → beta/scale-bar}/src/scaleBar.scss +0 -0
  241. /package/plugins/{scale-bar → beta/scale-bar}/src/utils.js +0 -0
  242. /package/plugins/{scale-bar → beta/scale-bar}/src/utils.test.js +0 -0
  243. /package/plugins/{use-location → beta/use-location}/dist/esm/index.js.LICENSE.txt +0 -0
  244. /package/plugins/{use-location → beta/use-location}/dist/umd/index.js.LICENSE.txt +0 -0
  245. /package/plugins/{use-location → beta/use-location}/src/UseLocation.jsx +0 -0
  246. /package/plugins/{use-location → beta/use-location}/src/UseLocationInit.jsx +0 -0
  247. /package/plugins/{use-location → beta/use-location}/src/defaults.js +0 -0
  248. /package/plugins/{use-location → beta/use-location}/src/events.js +0 -0
  249. /package/plugins/{use-location → beta/use-location}/src/index.js +0 -0
  250. /package/plugins/{use-location → beta/use-location}/src/manifest.js +0 -0
  251. /package/plugins/{use-location → beta/use-location}/src/reducer.js +0 -0
  252. /package/providers/{esri → beta/esri}/src/appEvents.js +0 -0
  253. /package/providers/{esri → beta/esri}/src/defaults.js +0 -0
  254. /package/providers/{esri → beta/esri}/src/esriProvider.js +0 -0
  255. /package/providers/{esri → beta/esri}/src/esriProvider.scss +0 -0
  256. /package/providers/{esri → beta/esri}/src/index.js +0 -0
  257. /package/providers/{esri → beta/esri}/src/utils/coords.js +0 -0
  258. /package/providers/{esri → beta/esri}/src/utils/detectWebGL.js +0 -0
  259. /package/providers/{esri → beta/esri}/src/utils/esriFixes.js +0 -0
  260. /package/providers/{esri → beta/esri}/src/utils/query.js +0 -0
  261. /package/providers/{esri → beta/esri}/src/utils/spatial.js +0 -0
  262. /package/providers/{open-names → beta/open-names}/dist/esm/im-reverse-geocode.js.LICENSE.txt +0 -0
  263. /package/providers/{open-names → beta/open-names}/dist/esm/index.js.LICENSE.txt +0 -0
  264. /package/providers/{open-names → beta/open-names}/dist/umd/im-reverse-geocode.js.LICENSE.txt +0 -0
  265. /package/providers/{open-names → beta/open-names}/dist/umd/index.js.LICENSE.txt +0 -0
  266. /package/providers/{open-names → beta/open-names}/src/index.js +0 -0
  267. /package/providers/{open-names → beta/open-names}/src/reverseGeocode.js +0 -0
  268. /package/providers/{open-names → beta/open-names}/src/utils/mapToLocationModel.js +0 -0
@@ -0,0 +1,792 @@
1
+ import DirectSelect from '../../../../../node_modules/@mapbox/mapbox-gl-draw/src/modes/direct_select.js'
2
+ import { spatialNavigate } from '../utils/spatial.js'
3
+ import {
4
+ getSnapInstance, isSnapActive, isSnapEnabled, getSnapLngLat,
5
+ getSnapRadius, triggerSnapAtPoint, clearSnapIndicator, clearSnapState
6
+ } from '../utils/snapHelpers.js'
7
+
8
+ const ARROW_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']
9
+ const ARROW_OFFSETS = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0] }
10
+ const NUDGE = 1, STEP = 5
11
+
12
+ const touchVertexTarget = `
13
+ <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>
14
+ <circle cx='24' cy='24' r='24' fill='currentColor'/>
15
+ <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"/>
16
+ </svg>
17
+ `
18
+
19
+ const scalePoint = (point, scale) => ({ x: point.x * scale, y: point.y * scale })
20
+ const isOnSVG = (el) => el instanceof window.SVGElement || el.ownerSVGElement
21
+
22
+ // Helper to get flat coordinates array from feature (handles both Polygon and LineString)
23
+ const getCoords = (feature) => feature?.type === 'LineString' ? feature.coordinates : feature?.coordinates?.flat(1)
24
+
25
+ // Helper to get the modifiable coordinates array from geojson (for undo operations)
26
+ const getModifiableCoords = (geojson) =>
27
+ geojson.geometry.type === 'Polygon' ? geojson.geometry.coordinates[0] : geojson.geometry.coordinates
28
+
29
+ export const EditVertexMode = {
30
+ ...DirectSelect,
31
+
32
+ onSetup(options) {
33
+ const state = DirectSelect.onSetup.call(this, options)
34
+ Object.assign(state, {
35
+ container: options.container,
36
+ interfaceType: options.interfaceType,
37
+ deleteVertexButtonId: options.deleteVertexButtonId,
38
+ undoButtonId: options.undoButtonId,
39
+ isPanEnabled: options.isPanEnabled,
40
+ getSnapEnabled: options.getSnapEnabled,
41
+ featureId: state.featureId || options.featureId,
42
+ selectedVertexIndex: options.selectedVertexIndex ?? -1,
43
+ selectedVertexType: options.selectedVertexType,
44
+ scale: options.scale ?? 1
45
+ })
46
+
47
+ // Get feature type for later reference
48
+ const feature = this.getFeature(state.featureId)
49
+ state.featureType = feature?.type
50
+
51
+ state.vertecies = this.getVerticies(state.featureId)
52
+ state.midpoints = this.getMidpoints(state.featureId)
53
+ this.setupEventListeners(state)
54
+
55
+ if (options.selectedVertexType === 'midpoint') {
56
+ this.updateMidpoint(state.midpoints[options.selectedVertexIndex - state.vertecies.length])
57
+ }
58
+ this.addTouchVertexTarget(state)
59
+
60
+ // Clear any snap indicator when entering edit mode
61
+ const snap = getSnapInstance(this.map)
62
+ if (snap) {
63
+ clearSnapIndicator(snap, this.map)
64
+ }
65
+
66
+ // Show touch target if entering with a selected vertex on touch interface
67
+ if (state.interfaceType === 'touch' && state.selectedVertexIndex >= 0 && state.selectedVertexType === 'vertex') {
68
+ const vertex = state.vertecies[state.selectedVertexIndex]
69
+ if (vertex) {
70
+ setTimeout(() => {
71
+ this.updateTouchVertexTarget(state, scalePoint(this.map.project(vertex), state.scale))
72
+ }, 0)
73
+ }
74
+ }
75
+
76
+ // Ignore pointermove deselection briefly after setup to let Safari settle
77
+ state._ignorePointermoveDeselect = true
78
+ setTimeout(() => { state._ignorePointermoveDeselect = false }, 100)
79
+
80
+ return state
81
+ },
82
+
83
+ setupEventListeners(state) {
84
+ const bind = (fn) => (e) => fn.call(this, state, e)
85
+ const h = this.handlers = {
86
+ keydown: bind(this.onKeydown), keyup: bind(this.onKeyup),
87
+ pointerdown: bind(this.onPointerevent), pointermove: bind(this.onPointerevent), pointerup: bind(this.onPointerevent),
88
+ click: bind(this.onButtonClick),
89
+ touchstart: bind(this.onTouchstart), touchmove: bind(this.onTouchmove), touchend: bind(this.onTouchend),
90
+ selectionchange: bind(this.onSelectionChange), scalechange: bind(this.onScaleChange),
91
+ update: bind(this.onUpdate), move: bind(this.onMove)
92
+ }
93
+
94
+ window.addEventListener('keydown', h.keydown, { capture: true })
95
+ window.addEventListener('keyup', h.keyup, { capture: true })
96
+ window.addEventListener('click', h.click)
97
+ state.container.addEventListener('pointerdown', h.pointerdown)
98
+ state.container.addEventListener('pointermove', h.pointermove)
99
+ state.container.addEventListener('pointerup', h.pointerup)
100
+ state.container.addEventListener('touchstart', h.touchstart, { passive: false })
101
+ state.container.addEventListener('touchmove', h.touchmove, { passive: false })
102
+ state.container.addEventListener('touchend', h.touchend, { passive: false })
103
+ this.map.on('draw.selectionchange', h.selectionchange)
104
+ this.map.on('draw.scalechange', h.scalechange)
105
+ this.map.on('draw.update', h.update)
106
+ this.map.on('move', h.move)
107
+ },
108
+
109
+ onSelectionChange(state, e) {
110
+ const vertexCoord = e.points[e.points.length - 1]?.geometry.coordinates
111
+ const geom = e.features[0].geometry
112
+ const coords = getCoords(geom)
113
+ const idx = coords.findIndex(c => vertexCoord && c[0] === vertexCoord[0] && c[1] === vertexCoord[1])
114
+
115
+ // Only update selectedVertexIndex from event if not keyboard mode AND event has valid vertex
116
+ if (state.interfaceType !== 'keyboard' && idx >= 0) {
117
+ state.selectedVertexIndex = idx
118
+ }
119
+ state.selectedVertexType ??= state.selectedVertexIndex >= 0 ? 'vertex' : null
120
+
121
+ this.map.fire('draw.vertexselection', {
122
+ index: state.selectedVertexType === 'vertex' ? state.selectedVertexIndex : -1,
123
+ numVertecies: state.vertecies.length
124
+ })
125
+
126
+ // Use vertex from event if available, otherwise fall back to state
127
+ const vertex = vertexCoord || (state.selectedVertexIndex >= 0 ? state.vertecies[state.selectedVertexIndex] : null)
128
+ this.updateTouchVertexTarget(state, vertex ? scalePoint(this.map.project(vertex), state.scale) : null)
129
+ },
130
+
131
+ onScaleChange(state, e) {
132
+ state.scale = e.scale
133
+ },
134
+
135
+ onUpdate(state) {
136
+ const prev = new Set(state.vertecies.map(c => JSON.stringify(c)))
137
+ if (prev.size === state.vertecies.length) {
138
+ return
139
+ }
140
+ state.selectedVertexIndex = state.vertecies.findIndex(c => !prev.has(JSON.stringify(c)))
141
+ state.selectedVertexType ??= state.selectedVertexIndex >= 0 ? 'vertex' : null
142
+ },
143
+
144
+ onKeydown(state, e) {
145
+ state.interfaceType = 'keyboard'
146
+ this.hideTouchVertexIndicator(state)
147
+
148
+ if (e.key === ' ' && state.selectedVertexIndex < 0) {
149
+ // Clear snap indicator when starting keyboard selection
150
+ const snap = getSnapInstance(this.map)
151
+ if (snap) {
152
+ clearSnapIndicator(snap, this.map)
153
+ }
154
+
155
+ // Ensure we have vertices to select
156
+ if (!state.vertecies?.length) {
157
+ state.vertecies = this.getVerticies(state.featureId)
158
+ state.midpoints = this.getMidpoints(state.featureId)
159
+ }
160
+ if (!state.vertecies?.length) {
161
+ return
162
+ }
163
+ state.isPanEnabled = false
164
+ return this.updateVertex(state)
165
+ }
166
+
167
+ if (!e.altKey && ARROW_KEYS.includes(e.key) && state.selectedVertexIndex >= 0) {
168
+ e.preventDefault()
169
+ e.stopPropagation()
170
+ if (state.selectedVertexType === 'midpoint') {
171
+ return this.insertVertex(state, e)
172
+ }
173
+
174
+ const snap = getSnapInstance(this.map)
175
+ const feature = this.getFeature(state.featureId)
176
+ if (!feature) {
177
+ return
178
+ }
179
+ const coords = getCoords(feature)
180
+ const currentCoord = coords?.[state.selectedVertexIndex]
181
+ if (!currentCoord) {
182
+ return
183
+ }
184
+
185
+ // Save starting position for undo (only on first move of sequence)
186
+ if (!state._keyboardMoveStartPosition) {
187
+ state._keyboardMoveStartPosition = [...currentCoord]
188
+ state._keyboardMoveStartIndex = state.selectedVertexIndex
189
+ }
190
+
191
+ // Break out of snap by moving outside snap radius
192
+ if (isSnapEnabled(state) && state._isSnapped && snap) {
193
+ const offset = getSnapRadius(snap) + 1
194
+ const pt = this.map.project(currentCoord)
195
+ const [dx, dy] = ARROW_OFFSETS[e.key].map(v => v * offset)
196
+ state._isSnapped = false
197
+ clearSnapIndicator(snap, this.map)
198
+ return this.moveVertex(state, this.map.unproject({ x: pt.x + dx, y: pt.y + dy }))
199
+ }
200
+
201
+ const newCoord = this.getNewCoord(state, e)
202
+ if (isSnapEnabled(state) && snap) {
203
+ triggerSnapAtPoint(snap, this.map, this.map.project(newCoord))
204
+ if (isSnapActive(snap)) {
205
+ state._isSnapped = true
206
+ return this.moveVertex(state, getSnapLngLat(snap))
207
+ }
208
+ }
209
+ state._isSnapped = false
210
+ return this.moveVertex(state, newCoord)
211
+ }
212
+
213
+ if (e.altKey && ARROW_KEYS.includes(e.key) && state.selectedVertexIndex >= 0) {
214
+ e.preventDefault()
215
+ e.stopPropagation()
216
+ return this.updateVertex(state, e.key)
217
+ }
218
+
219
+ if (e.key === 'Escape') {
220
+ this.changeMode(state, { isPanEnabled: true, selectedVertexIndex: -1, selectedVertexType: null })
221
+ }
222
+
223
+ // Undo with Cmd/Ctrl+Z (works without viewport focus, but not in input fields)
224
+ if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
225
+ const tag = document.activeElement?.tagName
226
+ if (tag === 'INPUT' || tag === 'TEXTAREA') {
227
+ return
228
+ }
229
+ e.preventDefault()
230
+ e.stopPropagation()
231
+ return this.handleUndo(state)
232
+ }
233
+ },
234
+
235
+ onKeyup(state, e) {
236
+ state.interfaceType = 'keyboard'
237
+ if (ARROW_KEYS.includes(e.key) && state.selectedVertexIndex >= 0) {
238
+ e.stopPropagation()
239
+
240
+ // Push undo for keyboard move sequence
241
+ if (state._keyboardMoveStartPosition && state._keyboardMoveStartIndex !== undefined) {
242
+ this.pushUndo({
243
+ type: 'move_vertex',
244
+ featureId: state.featureId,
245
+ vertexIndex: state._keyboardMoveStartIndex,
246
+ previousPosition: state._keyboardMoveStartPosition
247
+ })
248
+ state._keyboardMoveStartPosition = null
249
+ state._keyboardMoveStartIndex = undefined
250
+ }
251
+ }
252
+ if (e.key === 'Delete') {
253
+ this.deleteVertex(state)
254
+ }
255
+ },
256
+
257
+ onMouseDown(state, e) {
258
+ clearSnapState(getSnapInstance(this.map))
259
+ const meta = e.featureTarget?.properties.meta
260
+ const coordPath = e.featureTarget?.properties.coord_path
261
+
262
+ if (['vertex', 'midpoint'].includes(meta)) {
263
+ state.dragMoveLocation = e.lngLat
264
+ state.dragMoving = false
265
+ DirectSelect.onMouseDown.call(this, state, e)
266
+
267
+ // Save starting position for undo (only for vertices, not midpoints)
268
+ if (meta === 'vertex' && coordPath) {
269
+ // Extract vertex index from coord_path (e.g., "0.2" -> 2 for Polygon, "2" -> 2 for LineString)
270
+ const parts = coordPath.split('.')
271
+ const vertexIndex = Number.parseInt(parts[parts.length - 1], 10)
272
+ const vertex = state.vertecies?.[vertexIndex]
273
+ if (vertex) {
274
+ state._moveStartPosition = [...vertex]
275
+ state._moveStartIndex = vertexIndex
276
+ }
277
+ }
278
+ }
279
+ if (meta === 'midpoint') {
280
+ // DirectSelect converts midpoint to vertex - track this as an insert
281
+ // Get the new vertex index (midpoint at position N becomes vertex at N+1)
282
+ const parts = coordPath.split('.')
283
+ const insertedIndex = Number.parseInt(parts[parts.length - 1], 10)
284
+
285
+ // Track this insertion for undo (will be pushed on mouseUp if drag occurred)
286
+ state._insertedVertexIndex = insertedIndex
287
+ state._isInsertingVertex = true
288
+
289
+ state.selectedVertexIndex = this.getVertexIndexFromMidpoint(state, coordPath)
290
+ state.selectedVertexType = 'vertex'
291
+ this.map.fire('draw.vertexselection', { index: state.selectedVertexIndex, numVertecies: state.vertecies.length })
292
+ }
293
+ },
294
+
295
+ onMouseUp(state, e) {
296
+ clearSnapState(getSnapInstance(this.map))
297
+
298
+ // Check if vertex actually moved by comparing current position to start position
299
+ // This is more robust than relying on state.dragMoving which can be inconsistent
300
+ // IMPORTANT: Get current position from the feature, not state.vertecies (which is cached)
301
+ let vertexMoved = false
302
+ if (state._moveStartPosition && state._moveStartIndex !== undefined) {
303
+ const feature = this.getFeature(state.featureId)
304
+ if (feature) {
305
+ const currentVertex = getCoords(feature)?.[state._moveStartIndex]
306
+ if (currentVertex) {
307
+ vertexMoved = currentVertex[0] !== state._moveStartPosition[0] ||
308
+ currentVertex[1] !== state._moveStartPosition[1]
309
+ }
310
+ }
311
+ }
312
+
313
+ // Also check for insertions (dragMoving is reliable for midpoint drags)
314
+ const wasInsertion = state._isInsertingVertex && state._insertedVertexIndex !== undefined
315
+
316
+ if (state.dragMoving || vertexMoved || wasInsertion) {
317
+ this.syncVertices(state)
318
+
319
+ // Push undo for vertex insertion (from dragging midpoint)
320
+ if (wasInsertion) {
321
+ this.pushUndo({
322
+ type: 'insert_vertex',
323
+ featureId: state.featureId,
324
+ vertexIndex: state._insertedVertexIndex
325
+ })
326
+ state._isInsertingVertex = false
327
+ state._insertedVertexIndex = undefined
328
+ }
329
+ // Push undo for the move if vertex actually moved
330
+ else if (vertexMoved && state._moveStartPosition && state._moveStartIndex !== undefined) {
331
+ this.pushUndo({
332
+ type: 'move_vertex',
333
+ featureId: state.featureId,
334
+ vertexIndex: state._moveStartIndex,
335
+ previousPosition: state._moveStartPosition
336
+ })
337
+ }
338
+ }
339
+
340
+ // Clean up move state
341
+ state._moveStartPosition = null
342
+ state._moveStartIndex = null
343
+
344
+ DirectSelect.onMouseUp.call(this, state, e)
345
+ },
346
+
347
+ onPointerevent(state, e) {
348
+ state.interfaceType = e.pointerType === 'touch' ? 'touch' : 'pointer'
349
+ state.isPanEnabled = true
350
+ if (e.pointerType === 'touch' && e.type === 'pointermove' && !isOnSVG(e.target.parentNode) && !state._ignorePointermoveDeselect) {
351
+ this.changeMode(state, { selectedVertexIndex: -1, selectedVertexType: null, coordPath: null })
352
+ }
353
+ },
354
+
355
+ // Empty stubs required by DirectSelect
356
+ onTouchStart() {},
357
+ onTouchMove() {},
358
+ onTouchEnd() {},
359
+
360
+ onTouchend(state) {
361
+ clearSnapState(getSnapInstance(this.map))
362
+ if (state?.featureId) {
363
+ this.syncVertices(state)
364
+
365
+ // Push undo for the move if touch actually moved
366
+ if (state._touchMoved && state._moveStartPosition && state._moveStartIndex !== undefined) {
367
+ this.pushUndo({
368
+ type: 'move_vertex',
369
+ featureId: state.featureId,
370
+ vertexIndex: state._moveStartIndex,
371
+ previousPosition: state._moveStartPosition
372
+ })
373
+ }
374
+ state._moveStartPosition = null
375
+ state._moveStartIndex = undefined
376
+ state._touchMoved = false
377
+ }
378
+ },
379
+
380
+ clickNoTarget(state) {
381
+ this.changeMode(state, { selectedVertexIndex: -1, selectedVertexType: null, isPanEnabled: true })
382
+ },
383
+
384
+ onTap(state, e) {
385
+ // Hide snap indicator on any tap
386
+ const snap = getSnapInstance(this.map)
387
+ if (snap) {
388
+ clearSnapIndicator(snap, this.map)
389
+ }
390
+
391
+ const meta = e.featureTarget?.properties.meta
392
+ const coordPath = e.featureTarget?.properties.coord_path
393
+
394
+ if (meta === 'vertex') {
395
+ // Extract vertex index - handle both "0.1" (Polygon) and "1" (LineString) formats
396
+ const parts = coordPath.split('.')
397
+ const idx = Number.parseInt(parts[parts.length - 1], 10)
398
+ this.changeMode(state, {
399
+ selectedVertexIndex: idx,
400
+ selectedVertexType: 'vertex', coordPath
401
+ })
402
+ } else if (meta === 'midpoint') {
403
+ this.insertVertex({ ...state, selectedVertexIndex: this.getVertexIndexFromMidpoint(state, coordPath), selectedVertexType: 'midpoint' })
404
+ } else {
405
+ this.clickNoTarget(state)
406
+ }
407
+ },
408
+
409
+ onTouchstart(state, e) {
410
+ clearSnapState(getSnapInstance(this.map))
411
+ const vertex = state.vertecies?.[state.selectedVertexIndex]
412
+ if (!vertex || !isOnSVG(e.target.parentNode)) {
413
+ return
414
+ }
415
+
416
+ // Save starting position for undo
417
+ state._moveStartPosition = [...vertex]
418
+ state._moveStartIndex = state.selectedVertexIndex
419
+ state._touchMoved = false
420
+
421
+ const touch = { x: e.touches[0].clientX, y: e.touches[0].clientY }
422
+ const style = window.getComputedStyle(state.touchVertexTarget)
423
+ state.deltaTarget = { x: touch.x - Number.parseFloat(style.left), y: touch.y - Number.parseFloat(style.top) }
424
+ const vertexPt = this.map.project(vertex)
425
+ state.deltaVertex = { x: (touch.x / state.scale) - vertexPt.x, y: (touch.y / state.scale) - vertexPt.y }
426
+ },
427
+
428
+ onTouchmove(state, e) {
429
+ if (state.selectedVertexIndex < 0 || !isOnSVG(e.target.parentNode)) {
430
+ return
431
+ }
432
+
433
+ state._touchMoved = true
434
+
435
+ const touch = { x: e.touches[0].clientX, y: e.touches[0].clientY }
436
+ const screenPt = { x: (touch.x / state.scale) - state.deltaVertex.x, y: (touch.y / state.scale) - state.deltaVertex.y }
437
+
438
+ let finalCoord = this.map.unproject(screenPt)
439
+ if (isSnapEnabled(state)) {
440
+ const snap = getSnapInstance(this.map)
441
+ triggerSnapAtPoint(snap, this.map, screenPt)
442
+ finalCoord = getSnapLngLat(snap) || finalCoord
443
+ }
444
+
445
+ this.moveVertex(state, finalCoord)
446
+ this.updateTouchVertexTarget(state, { x: touch.x - state.deltaTarget.x, y: touch.y - state.deltaTarget.y })
447
+ },
448
+
449
+ onDrag(state, e) {
450
+ if (state.interfaceType === 'touch') {
451
+ return
452
+ }
453
+
454
+ this.map.fire('draw.geometrychange', state.feature)
455
+
456
+ const snap = getSnapInstance(this.map)
457
+ if (snap) {
458
+ snap.snapStatus = false
459
+ snap.snapCoords = null
460
+ }
461
+
462
+ if (!isSnapEnabled(state) || !snap?.status) {
463
+ DirectSelect.onDrag.call(this, state, e)
464
+ return
465
+ }
466
+
467
+ if (!state.selectedCoordPaths?.length || !state.canDragMove) {
468
+ return
469
+ }
470
+
471
+ state.dragMoving = true
472
+ e.originalEvent.stopPropagation()
473
+ triggerSnapAtPoint(snap, this.map, e.point)
474
+
475
+ const finalLngLat = getSnapLngLat(snap) || e.lngLat
476
+ state.feature.updateCoordinate(state.selectedCoordPaths[0], finalLngLat.lng, finalLngLat.lat)
477
+ state.dragMoveLocation = e.lngLat
478
+ },
479
+
480
+ onMove(state) {
481
+ const vertex = state.vertecies[state.selectedVertexIndex]
482
+ if (vertex) {
483
+ this.updateTouchVertexTarget(state, scalePoint(this.map.project(vertex), state.scale))
484
+ }
485
+ },
486
+
487
+ onButtonClick(state, e) {
488
+ if (e.target.closest(`#${state.deleteVertexButtonId}`) && state.selectedVertexType === 'vertex') {
489
+ this.deleteVertex(state)
490
+ }
491
+ if (e.target.closest(`#${state.undoButtonId}`)) {
492
+ this.handleUndo(state)
493
+ }
494
+ },
495
+
496
+ handleUndo(state) {
497
+ const undoStack = this.map._undoStack
498
+ if (!undoStack || undoStack.length === 0) {
499
+ return
500
+ }
501
+
502
+ const op = undoStack.pop()
503
+
504
+ if (op.type === 'move_vertex') {
505
+ this.undoMoveVertex(state, op)
506
+ } else if (op.type === 'insert_vertex') {
507
+ this.undoInsertVertex(state, op)
508
+ } else if (op.type === 'delete_vertex') {
509
+ this.undoDeleteVertex(state, op)
510
+ } else {
511
+ // No action
512
+ }
513
+ },
514
+
515
+ // Utility methods
516
+ getCoordPath(state, idx) {
517
+ // Use cached featureType or look it up
518
+ const type = state.featureType || this.getFeature(state.featureId)?.type
519
+ return type === 'LineString' ? `${idx}` : `0.${idx}`
520
+ },
521
+
522
+ syncVertices(state) {
523
+ state.vertecies = this.getVerticies(state.featureId)
524
+ state.midpoints = this.getMidpoints(state.featureId)
525
+ },
526
+
527
+ getVerticies(featureId) {
528
+ return getCoords(this.getFeature(featureId)) || []
529
+ },
530
+
531
+ getMidpoints(featureId) {
532
+ const feature = this.getFeature(featureId)
533
+ const coords = getCoords(feature)
534
+ if (!coords) {
535
+ return []
536
+ }
537
+
538
+ // For lines, don't create midpoint between last and first vertex
539
+ const count = feature.type === 'LineString' ? coords.length - 1 : coords.length
540
+ const midpoints = []
541
+ for (let i = 0; i < count; i++) {
542
+ const next = coords[(i + 1) % coords.length]
543
+ midpoints.push([(coords[i][0] + next[0]) / 2, (coords[i][1] + next[1]) / 2])
544
+ }
545
+ return midpoints
546
+ },
547
+
548
+ getVertexOrMidpoint(state, direction) {
549
+ // Ensure vertices and midpoints are populated
550
+ if (!state.vertecies?.length) {
551
+ state.vertecies = this.getVerticies(state.featureId)
552
+ state.midpoints = this.getMidpoints(state.featureId)
553
+ }
554
+ if (!state.vertecies?.length) {
555
+ return [-1, null]
556
+ }
557
+ const project = (p) => p ? Object.values(this.map.project(p)) : null
558
+ const pixels = [...state.vertecies.map(project), ...state.midpoints.map(project)].filter(Boolean)
559
+ if (!pixels.length) {
560
+ return [-1, null]
561
+ }
562
+ const start = pixels[state.selectedVertexIndex] || Object.values(this.map.project(this.map.getCenter()))
563
+ const idx = spatialNavigate(start, pixels, direction)
564
+ return [idx, idx < state.vertecies.length ? 'vertex' : 'midpoint']
565
+ },
566
+
567
+ getVertexIndexFromMidpoint(state, coordPath) {
568
+ // Handle both "0.1" (Polygon) and "1" (LineString) formats
569
+ const parts = coordPath.split('.')
570
+ const afterIdx = Number.parseInt(parts[parts.length - 1], 10)
571
+
572
+ // Get feature type from cache or look it up
573
+ const featureType = state.featureType || this.getFeature(state.featureId)?.type
574
+
575
+ // For LineString, coord_path is insertion position (1-indexed), so subtract 1 for midpoint array index
576
+ // For Polygon, adjust for ring wrapping
577
+ if (featureType === 'LineString') {
578
+ return state.vertecies.length + (afterIdx - 1)
579
+ }
580
+ return state.vertecies.length + ((afterIdx - 1 + state.vertecies.length) % state.vertecies.length)
581
+ },
582
+
583
+ addTouchVertexTarget(state) {
584
+ let el = state.container.querySelector('[data-touch-vertex-target]')
585
+ if (!el) {
586
+ state.container.insertAdjacentHTML('beforeend', touchVertexTarget)
587
+ el = state.container.querySelector('[data-touch-vertex-target]')
588
+ }
589
+ state.touchVertexTarget = el
590
+ },
591
+
592
+ updateTouchVertexTarget(state, point) {
593
+ if (point && state.interfaceType === 'touch' && state.selectedVertexIndex >= 0) {
594
+ Object.assign(state.touchVertexTarget.style, { display: 'block', top: `${point.y}px`, left: `${point.x}px` })
595
+ } else {
596
+ state.touchVertexTarget.style.display = 'none'
597
+ }
598
+ },
599
+
600
+ hideTouchVertexIndicator(state) {
601
+ state.touchVertexTarget.style.display = 'none'
602
+ },
603
+
604
+ updateMidpoint(coordinates) {
605
+ setTimeout(() => {
606
+ this.map.getSource('mapbox-gl-draw-hot').setData({
607
+ type: 'Feature',
608
+ properties: { meta: 'midpoint', active: 'true', id: 'active-midpoint' },
609
+ geometry: { type: 'Point', coordinates }
610
+ })
611
+ }, 0)
612
+ },
613
+
614
+ updateVertex(state, direction) {
615
+ const [idx, type] = this.getVertexOrMidpoint(state, direction)
616
+ if (idx < 0 || !type) {
617
+ return
618
+ }
619
+ this.changeMode(state, { selectedVertexIndex: idx, selectedVertexType: type, ...(type === 'vertex' && { coordPath: this.getCoordPath(state, idx) }) })
620
+ },
621
+
622
+ getOffset(coord, e) {
623
+ const pt = this.map.project(coord)
624
+ const offset = e?.shiftKey ? NUDGE : STEP
625
+ const [dx, dy] = e ? ARROW_OFFSETS[e.key].map(v => v * offset) : [0, 0]
626
+ return this.map.unproject({ x: pt.x + dx, y: pt.y + dy })
627
+ },
628
+
629
+ getNewCoord(state, e) {
630
+ return this.getOffset(getCoords(this.getFeature(state.featureId))[state.selectedVertexIndex], e)
631
+ },
632
+
633
+ insertVertex(state, e) {
634
+ const midIdx = state.selectedVertexIndex - state.vertecies.length
635
+ const newCoord = this.getOffset(state.midpoints[midIdx], e)
636
+ const geojson = this.getFeature(state.featureId).toGeoJSON()
637
+ const newIdx = midIdx + 1
638
+ getModifiableCoords(geojson).splice(newIdx, 0, [newCoord.lng, newCoord.lat])
639
+ this._ctx.api.add(geojson)
640
+
641
+ this.pushUndo({ type: 'insert_vertex', featureId: state.featureId, vertexIndex: newIdx })
642
+ this.changeMode(state, { selectedVertexIndex: newIdx, selectedVertexType: 'vertex', coordPath: this.getCoordPath(state, newIdx) })
643
+ },
644
+
645
+ moveVertex(state, coord, options = {}) {
646
+ if (options.checkSnap && state.enableSnap !== false) {
647
+ const snap = this.map._snapInstance
648
+ if (snap?.snapStatus && snap.snapCoords?.length >= 2) {
649
+ coord = { lng: snap.snapCoords[0], lat: snap.snapCoords[1] }
650
+ }
651
+ }
652
+ const geojson = this.getFeature(state.featureId).toGeoJSON()
653
+ getModifiableCoords(geojson)[state.selectedVertexIndex] = [coord.lng, coord.lat]
654
+ this._ctx.api.add(geojson)
655
+ state.vertecies = this.getVerticies(state.featureId)
656
+
657
+ this.map.fire('draw.geometrychange', state.feature)
658
+ },
659
+
660
+ deleteVertex(state) {
661
+ const featureType = state.featureType || this.getFeature(state.featureId)?.type
662
+ // Minimum vertices: 3 for Polygon, 2 for LineString
663
+ const minVertices = featureType === 'Polygon' ? 3 : 2
664
+ if (state.vertecies.length <= minVertices) {
665
+ return
666
+ }
667
+
668
+ // Save position for undo before deletion
669
+ const deletedPosition = [...state.vertecies[state.selectedVertexIndex]]
670
+ const deletedIndex = state.selectedVertexIndex
671
+
672
+ const nextIdx = state.selectedVertexIndex >= state.vertecies.length - 1 ? state.selectedVertexIndex - 1 : state.selectedVertexIndex
673
+ this._ctx.api.trash()
674
+
675
+ // Push undo operation
676
+ this.pushUndo({
677
+ type: 'delete_vertex',
678
+ featureId: state.featureId,
679
+ vertexIndex: deletedIndex,
680
+ position: deletedPosition
681
+ })
682
+
683
+ this.changeMode(state, { selectedVertexIndex: nextIdx, selectedVertexType: 'vertex', coordPath: this.getCoordPath(state, nextIdx) })
684
+ },
685
+
686
+ // Prevent selecting other features
687
+ changeMode(state, updates) {
688
+ if (!state.featureId) {
689
+ return
690
+ }
691
+ this._ctx.api.changeMode('edit_vertex', { ...state, ...updates })
692
+ },
693
+
694
+ // Fire geometry change event (for external listeners)
695
+ fireGeometryChange(state) {
696
+ const feature = this.getFeature(state.featureId)
697
+ if (feature) {
698
+ this.map.fire('draw.update', {
699
+ features: [feature.toGeoJSON()],
700
+ action: 'change_coordinates'
701
+ })
702
+ }
703
+ },
704
+
705
+ // Undo support
706
+ pushUndo(operation) {
707
+ const undoStack = this.map._undoStack
708
+ if (!undoStack) {
709
+ return
710
+ }
711
+ undoStack.push(operation)
712
+ },
713
+
714
+ undoMoveVertex(state, op) {
715
+ const { vertexIndex, previousPosition, featureId } = op
716
+ const feature = this.getFeature(featureId)
717
+ if (!feature) {
718
+ return
719
+ }
720
+
721
+ const geojson = feature.toGeoJSON()
722
+ getModifiableCoords(geojson)[vertexIndex] = previousPosition
723
+ this._applyUndoAndSync(state, geojson, featureId)
724
+
725
+ // Update touch vertex target position
726
+ const vertex = state.vertecies[state.selectedVertexIndex]
727
+ if (vertex) {
728
+ this.updateTouchVertexTarget(state, scalePoint(this.map.project(vertex), state.scale))
729
+ }
730
+ },
731
+
732
+ undoInsertVertex(state, op) {
733
+ const { vertexIndex, featureId } = op
734
+ const feature = this.getFeature(featureId)
735
+ if (!feature) {
736
+ return
737
+ }
738
+
739
+ const geojson = feature.toGeoJSON()
740
+ getModifiableCoords(geojson).splice(vertexIndex, 1)
741
+ this._applyUndoAndSync(state, geojson, featureId)
742
+
743
+ this.hideTouchVertexIndicator(state)
744
+ this.changeMode(state, { selectedVertexIndex: -1, selectedVertexType: null })
745
+ },
746
+
747
+ undoDeleteVertex(state, op) {
748
+ const { vertexIndex, position, featureId } = op
749
+ const feature = this.getFeature(featureId)
750
+ if (!feature) {
751
+ return
752
+ }
753
+
754
+ const geojson = feature.toGeoJSON()
755
+ getModifiableCoords(geojson).splice(vertexIndex, 0, position)
756
+ this._applyUndoAndSync(state, geojson, featureId)
757
+
758
+ // Update touch vertex target to restored vertex position
759
+ const vertex = state.vertecies[vertexIndex]
760
+ if (vertex) {
761
+ this.updateTouchVertexTarget(state, scalePoint(this.map.project(vertex), state.scale))
762
+ }
763
+ this.changeMode(state, { selectedVertexIndex: vertexIndex, selectedVertexType: 'vertex', coordPath: this.getCoordPath(state, vertexIndex) })
764
+ },
765
+
766
+ _applyUndoAndSync(state, geojson, featureId) {
767
+ this._ctx.api.add(geojson)
768
+ state.vertecies = this.getVerticies(featureId)
769
+ state.midpoints = this.getMidpoints(featureId)
770
+ this._ctx.store.render()
771
+ this.fireGeometryChange(state)
772
+ },
773
+
774
+ onStop(state) {
775
+ const h = this.handlers
776
+ state.container.removeEventListener('pointerdown', h.pointerdown)
777
+ state.container.removeEventListener('pointermove', h.pointermove)
778
+ state.container.removeEventListener('pointerup', h.pointerup)
779
+ state.container.removeEventListener('touchstart', h.touchstart)
780
+ state.container.removeEventListener('touchmove', h.touchmove)
781
+ state.container.removeEventListener('touchend', h.touchend)
782
+ this.map.off('draw.selectionchange', h.selectionchange)
783
+ this.map.off('draw.scalechange', h.scalechange)
784
+ this.map.off('draw.update', h.update)
785
+ this.map.off('move', h.move)
786
+ this.map.dragPan.enable()
787
+ window.removeEventListener('click', h.click)
788
+ window.removeEventListener('keydown', h.keydown, { capture: true })
789
+ window.removeEventListener('keyup', h.keyup, { capture: true })
790
+ this.hideTouchVertexIndicator(state)
791
+ }
792
+ }