@defra/interactive-map 0.0.4-alpha → 0.0.6-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 (203) 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 +99 -0
  65. package/plugins/interact/src/utils/spatial.test.js +100 -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 +21 -18
  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 +28 -1
  164. package/src/App/store/appActionsMap.test.js +26 -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/_colors.scss +1 -1
  184. package/src/scss/settings/_dimensions.scss +4 -0
  185. package/src/services/eventBus.test.js +24 -1
  186. package/src/test-utils.js +1 -0
  187. package/src/types.js +5 -0
  188. package/src/utils/detectBreakpoint.js +78 -77
  189. package/src/utils/detectBreakpoint.test.js +50 -4
  190. package/src/utils/detectInterfaceType.js +6 -5
  191. package/src/utils/getIsFullscreen.js +3 -1
  192. package/src/utils/getSafeZoneInset.js +23 -7
  193. package/src/utils/getSafeZoneInset.test.js +18 -0
  194. package/src/utils/mapStateSync.js +3 -3
  195. package/src/utils/queryString.js +1 -1
  196. package/src/utils/stringToKebab.js +1 -1
  197. package/src/utils/toggleInertElements.js +37 -12
  198. package/webpack.dev.mjs +3 -4
  199. package/plugins/interact/src/utils/turfHelpers.js +0 -30
  200. package/plugins/interact/src/utils/turfHelpers.test.js +0 -30
  201. package/plugins/search/src/utils/fetchDataset.js +0 -23
  202. package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +0 -1
  203. package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +0 -1
@@ -0,0 +1,141 @@
1
+ import {
2
+ getCoords,
3
+ getRingSegments,
4
+ getSegmentForIndex,
5
+ getModifiableCoords
6
+ } from './geometryHelpers.js'
7
+
8
+ const ARROW_OFFSETS = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0] }
9
+ const NUDGE = 1, STEP = 5
10
+
11
+ export const vertexOperations = {
12
+ updateMidpoint(coordinates) {
13
+ setTimeout(() => {
14
+ this.map.getSource('mapbox-gl-draw-hot').setData({
15
+ type: 'Feature',
16
+ properties: { meta: 'midpoint', active: 'true', id: 'active-midpoint' },
17
+ geometry: { type: 'Point', coordinates }
18
+ })
19
+ }, 0)
20
+ },
21
+
22
+ updateVertex(state, direction) {
23
+ const [idx, type] = this.getVertexOrMidpoint(state, direction)
24
+ if (idx < 0 || !type) {
25
+ return
26
+ }
27
+ this.changeMode(state, { selectedVertexIndex: idx, selectedVertexType: type, ...(type === 'vertex' && { coordPath: this.getCoordPath(state, idx) }) })
28
+ },
29
+
30
+ getOffset(coord, e) {
31
+ const pt = this.map.project(coord)
32
+ const offset = e?.shiftKey ? NUDGE : STEP
33
+ const [dx, dy] = e ? ARROW_OFFSETS[e.key].map(v => v * offset) : [0, 0]
34
+ return this.map.unproject({ x: pt.x + dx, y: pt.y + dy })
35
+ },
36
+
37
+ getNewCoord(state, e) {
38
+ return this.getOffset(getCoords(this.getFeature(state.featureId))[state.selectedVertexIndex], e)
39
+ },
40
+
41
+ insertVertex(state, e) {
42
+ const midIdx = state.selectedVertexIndex - state.vertecies.length
43
+ const newCoord = this.getOffset(state.midpoints[midIdx], e)
44
+ const feature = this.getFeature(state.featureId)
45
+ const geojson = feature.toGeoJSON()
46
+
47
+ // Find which segment this midpoint belongs to and calculate insertion position
48
+ const segments = getRingSegments(feature)
49
+ let globalInsertIdx = midIdx + 1
50
+ let insertSegment = null
51
+ let localInsertIdx = 0
52
+
53
+ // Map midpoint index to segment and local position
54
+ let midpointCounter = 0
55
+ for (const seg of segments) {
56
+ // Must match getMidpoints calculation
57
+ const segMidpoints = seg.closed ? seg.length : seg.length - 1
58
+ if (midIdx < midpointCounter + segMidpoints) {
59
+ insertSegment = seg
60
+ localInsertIdx = (midIdx - midpointCounter) + 1
61
+ globalInsertIdx = seg.start + localInsertIdx
62
+ break
63
+ }
64
+ midpointCounter += segMidpoints
65
+ }
66
+
67
+ if (!insertSegment) return
68
+
69
+ const coords = getModifiableCoords(geojson, insertSegment.path)
70
+ coords.splice(localInsertIdx, 0, [newCoord.lng, newCoord.lat])
71
+ this._ctx.api.add(geojson)
72
+
73
+ this.pushUndo({ type: 'insert_vertex', featureId: state.featureId, vertexIndex: globalInsertIdx })
74
+ this.changeMode(state, { selectedVertexIndex: globalInsertIdx, selectedVertexType: 'vertex', coordPath: this.getCoordPath(state, globalInsertIdx) })
75
+ },
76
+
77
+ moveVertex(state, coord, options = {}) {
78
+ if (options.checkSnap && state.enableSnap !== false) {
79
+ const snap = this.map._snapInstance
80
+ if (snap?.snapStatus && snap.snapCoords?.length >= 2) {
81
+ coord = { lng: snap.snapCoords[0], lat: snap.snapCoords[1] }
82
+ }
83
+ }
84
+
85
+ const feature = this.getFeature(state.featureId)
86
+ const geojson = feature.toGeoJSON()
87
+ const segments = getRingSegments(feature)
88
+ const result = getSegmentForIndex(segments, state.selectedVertexIndex)
89
+ if (!result) return
90
+
91
+ const coords = getModifiableCoords(geojson, result.segment.path)
92
+ coords[result.localIdx] = [coord.lng, coord.lat]
93
+ this._ctx.api.add(geojson)
94
+ state.vertecies = this.getVerticies(state.featureId)
95
+
96
+ this.map.fire('draw.geometrychange', state.feature)
97
+ },
98
+
99
+ deleteVertex(state) {
100
+ const feature = this.getFeature(state.featureId)
101
+ if (!feature) {
102
+ return
103
+ }
104
+
105
+ const segments = getRingSegments(feature)
106
+ const result = getSegmentForIndex(segments, state.selectedVertexIndex)
107
+ if (!result) {
108
+ return
109
+ }
110
+
111
+ const { segment } = result
112
+ // Minimum vertices per segment: 3 for closed rings (mapbox-gl-draw's internal representation), 2 for lines
113
+ const minVertices = segment.closed ? 3 : 2
114
+ if (segment.length <= minVertices) {
115
+ return
116
+ }
117
+
118
+ // Save position for undo before deletion
119
+ const deletedPosition = [...state.vertecies[state.selectedVertexIndex]]
120
+ const deletedIndex = state.selectedVertexIndex
121
+
122
+ this._ctx.api.trash()
123
+
124
+ // Clear DirectSelect's coordinate selection to prevent visual artifacts
125
+ this.clearSelectedCoordinates()
126
+ // Force feature re-render to clear vertex highlights
127
+ feature.changed()
128
+ this._ctx.store.render()
129
+
130
+ // Push undo operation
131
+ this.pushUndo({
132
+ type: 'delete_vertex',
133
+ featureId: state.featureId,
134
+ vertexIndex: deletedIndex,
135
+ position: deletedPosition
136
+ })
137
+
138
+ // Clear selection after delete
139
+ this.changeMode(state, { selectedVertexIndex: -1, selectedVertexType: null })
140
+ }
141
+ }
@@ -0,0 +1,121 @@
1
+ import {
2
+ getCoords,
3
+ getRingSegments,
4
+ getSegmentForIndex
5
+ } from './geometryHelpers.js'
6
+ import { spatialNavigate } from '../../utils/spatial.js'
7
+
8
+ export const vertexQueries = {
9
+ findVertexIndex(coords, targetCoord, currentIdx) {
10
+ // Search for vertex, preferring matches near currentIdx to handle duplicate coords (e.g., closing vertices)
11
+ const matches = []
12
+ coords.forEach((c, i) => {
13
+ if (c[0] === targetCoord[0] && c[1] === targetCoord[1]) {
14
+ matches.push(i)
15
+ }
16
+ })
17
+
18
+ if (matches.length === 0) return -1
19
+ if (matches.length === 1) return matches[0]
20
+
21
+ // Multiple matches - pick closest to current selection
22
+ if (currentIdx >= 0) {
23
+ return matches.reduce((best, idx) =>
24
+ Math.abs(idx - currentIdx) < Math.abs(best - currentIdx) ? idx : best
25
+ )
26
+ }
27
+ return matches[0]
28
+ },
29
+
30
+ getCoordPath(state, idx) {
31
+ const feature = this.getFeature(state.featureId)
32
+ if (!feature) return '0'
33
+
34
+ const segments = getRingSegments(feature)
35
+ const result = getSegmentForIndex(segments, idx)
36
+ if (!result) return '0'
37
+
38
+ const { segment, localIdx } = result
39
+ return [...segment.path, localIdx].join('.')
40
+ },
41
+
42
+ syncVertices(state) {
43
+ state.vertecies = this.getVerticies(state.featureId)
44
+ state.midpoints = this.getMidpoints(state.featureId)
45
+ },
46
+
47
+ getVerticies(featureId) {
48
+ return getCoords(this.getFeature(featureId)) || []
49
+ },
50
+
51
+ getMidpoints(featureId) {
52
+ const feature = this.getFeature(featureId)
53
+ const coords = getCoords(feature)
54
+ const segments = getRingSegments(feature)
55
+ if (!coords?.length || !segments.length) {
56
+ return []
57
+ }
58
+
59
+ const midpoints = []
60
+ // Create midpoints within each segment, respecting boundaries
61
+ for (const seg of segments) {
62
+ // For closed rings, create midpoint between every vertex including last→first
63
+ // For open lines, create midpoints only between consecutive vertices (no wrap-around)
64
+ const count = seg.closed ? seg.length : seg.length - 1
65
+ for (let i = 0; i < count; i++) {
66
+ const idx = seg.start + i
67
+ const nextIdx = seg.start + ((i + 1) % seg.length)
68
+ const [x1, y1] = coords[idx]
69
+ const [x2, y2] = coords[nextIdx]
70
+ midpoints.push([(x1 + x2) / 2, (y1 + y2) / 2])
71
+ }
72
+ }
73
+ return midpoints
74
+ },
75
+
76
+ getVertexOrMidpoint(state, direction) {
77
+ // Ensure vertices and midpoints are populated
78
+ if (!state.vertecies?.length) {
79
+ state.vertecies = this.getVerticies(state.featureId)
80
+ state.midpoints = this.getMidpoints(state.featureId)
81
+ }
82
+ if (!state.vertecies?.length) {
83
+ return [-1, null]
84
+ }
85
+ const project = (p) => p ? Object.values(this.map.project(p)) : null
86
+ const pixels = [...state.vertecies.map(project), ...state.midpoints.map(project)].filter(Boolean)
87
+ if (!pixels.length) {
88
+ return [-1, null]
89
+ }
90
+ const start = pixels[state.selectedVertexIndex] || Object.values(this.map.project(this.map.getCenter()))
91
+ const idx = spatialNavigate(start, pixels, direction)
92
+ return [idx, idx < state.vertecies.length ? 'vertex' : 'midpoint']
93
+ },
94
+
95
+ getVertexIndexFromMidpoint(state, coordPath) {
96
+ const feature = this.getFeature(state.featureId)
97
+ const segments = getRingSegments(feature)
98
+ const parts = coordPath.split('.').map(Number)
99
+
100
+ // Find which segment this coord_path belongs to
101
+ let midpointOffset = 0
102
+ for (const seg of segments) {
103
+ const pathMatches = seg.path.every((val, idx) => val === parts[idx])
104
+ if (pathMatches && parts.length === seg.path.length + 1) {
105
+ // In DirectSelect, midpoint coord_path represents the insertion index
106
+ // The midpoint between vertex N and N+1 has coord_path ending in N+1
107
+ // So our flat midpoint index is one less than the coord_path index
108
+ const insertionIdx = parts[parts.length - 1]
109
+ const localMidpointIdx = insertionIdx > 0 ? insertionIdx - 1 : seg.length - 2
110
+ // Midpoints are indexed after all vertices
111
+ return state.vertecies.length + midpointOffset + localMidpointIdx
112
+ }
113
+ // Count midpoints in this segment (must match getMidpoints calculation)
114
+ const segMidpoints = seg.closed ? seg.length : seg.length - 1
115
+ midpointOffset += segMidpoints
116
+ }
117
+
118
+ // Fallback
119
+ return state.vertecies.length
120
+ }
121
+ }