Skip to content

Commit 943284c

Browse files
authored
Add insert waypoint button between mission items (#1180)
* Add insert waypoint button between mission items * Address copilot review comments
1 parent a4745ab commit 943284c

7 files changed

Lines changed: 429 additions & 54 deletions

File tree

gcs/src/components/mapComponents/fenceItems.jsx

Lines changed: 105 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,31 @@
55
connecting them. It properly parses the type of fence marker.
66
*/
77

8-
import { useEffect, useState } from "react"
8+
import { useMemo } from "react"
99

1010
// Helper imports
11-
import { intToCoord } from "../../helpers/dataFormatters"
11+
import { coordToInt, intToCoord } from "../../helpers/dataFormatters"
1212

1313
// Styling imports
1414
import "maplibre-gl/dist/maplibre-gl.css"
1515

1616
// Component imports
1717

1818
// Tailwind styling
19-
import { circle } from "@turf/turf"
19+
import { circle, midpoint, point } from "@turf/turf"
2020
import { Layer, Source } from "react-map-gl"
21-
import { useSelector } from "react-redux"
21+
import { useDispatch, useSelector } from "react-redux"
2222
import resolveConfig from "tailwindcss/resolveConfig"
2323
import tailwindConfig from "../../../tailwind.config"
2424
import { FENCE_ITEM_COMMANDS_LIST } from "../../helpers/mavlinkConstants"
2525
import { selectCurrentPage } from "../../redux/slices/droneConnectionSlice"
26-
import { selectActiveTab } from "../../redux/slices/missionSlice"
26+
import {
27+
insertFencePolygonVertex,
28+
selectActiveTab,
29+
} from "../../redux/slices/missionSlice"
2730
import DrawLineCoordinates from "./drawLineCoordinates"
2831
import MarkerPin from "./markerPin"
32+
import MidpointInsertButton from "./midpointInsertButton"
2933
const tailwindColors = resolveConfig(tailwindConfig).theme.colors
3034

3135
function getFenceCommandNumber(value) {
@@ -46,33 +50,87 @@ const circleCommands = [
4650
]
4751

4852
export default function FenceItems({ fenceItems }) {
53+
const dispatch = useDispatch()
4954
const currentPage = useSelector(selectCurrentPage)
5055
const editable =
5156
useSelector(selectActiveTab) === "fence" && currentPage === "missions"
5257

53-
const [fencePolygonItems, setFencePolygonItems] = useState([])
54-
const [fenceCircleItems, setFenceCircleItems] = useState([])
58+
const { fencePolygonGroups, fencePolygonItems, fenceCircleItems } =
59+
useMemo(() => {
60+
const polygonItems = []
61+
const circleItems = []
62+
const polygonGroups = []
5563

56-
useEffect(() => {
57-
// Filter out fence items based on their type
58-
const polygonItems = fenceItems.filter((item) =>
59-
polygonCommands.includes(item.command),
60-
)
61-
const circleItems = fenceItems.filter((item) =>
62-
circleCommands.includes(item.command),
63-
)
64+
let currentGroup = []
65+
let currentGroupStartIndex = null
6466

65-
setFencePolygonItems(polygonItems)
66-
setFenceCircleItems(circleItems)
67-
}, [fenceItems])
67+
fenceItems.forEach((item, index) => {
68+
if (polygonCommands.includes(item.command)) {
69+
const polygonItem = { ...item, fenceIndex: index }
70+
polygonItems.push(polygonItem)
71+
72+
if (currentGroup.length === 0) {
73+
currentGroupStartIndex = index
74+
}
75+
76+
currentGroup.push(polygonItem)
77+
78+
if (currentGroup.length === item.param1) {
79+
polygonGroups.push({
80+
items: currentGroup,
81+
startIndex: currentGroupStartIndex,
82+
})
83+
currentGroup = []
84+
currentGroupStartIndex = null
85+
}
86+
87+
return
88+
}
89+
90+
if (circleCommands.includes(item.command)) {
91+
circleItems.push({ ...item, fenceIndex: index })
92+
}
93+
})
94+
95+
return {
96+
fencePolygonGroups: polygonGroups,
97+
fencePolygonItems: polygonItems,
98+
fenceCircleItems: circleItems,
99+
}
100+
}, [fenceItems])
101+
102+
const polygonEdgeInsertButtons = useMemo(() => {
103+
if (!editable) return []
104+
105+
return fencePolygonGroups.flatMap((polygon) => {
106+
if (polygon.items.length < 2) return []
107+
108+
return polygon.items.map((item, index) => {
109+
const nextItem = polygon.items[(index + 1) % polygon.items.length]
110+
const midpointCoords = midpoint(
111+
point([intToCoord(item.y), intToCoord(item.x)]),
112+
point([intToCoord(nextItem.y), intToCoord(nextItem.x)]),
113+
).geometry.coordinates
114+
115+
return {
116+
afterId: item.id,
117+
polygonStartIndex: polygon.startIndex,
118+
polygonLength: polygon.items.length,
119+
lat: midpointCoords[1],
120+
lon: midpointCoords[0],
121+
tooltipText: `Insert vertex between ${item.z + 1} and ${nextItem.z + 1}`,
122+
}
123+
})
124+
})
125+
}, [editable, fencePolygonGroups])
68126

69127
return (
70128
<>
71129
{/* Show mission geo-fence MARKERS */}
72130
{fencePolygonItems.map((item, index) => {
73131
return (
74132
<MarkerPin
75-
key={index}
133+
key={item.id || index}
76134
id={item.id}
77135
lat={intToCoord(item.x)}
78136
lon={intToCoord(item.y)}
@@ -82,25 +140,31 @@ export default function FenceItems({ fenceItems }) {
82140
)
83141
})}
84142

143+
{polygonEdgeInsertButtons.map((button) => (
144+
<MidpointInsertButton
145+
key={`${button.afterId}:${button.polygonStartIndex}`}
146+
lat={button.lat}
147+
lon={button.lon}
148+
colour={tailwindColors.blue[400]}
149+
tooltipText={button.tooltipText}
150+
onClick={() => {
151+
dispatch(
152+
insertFencePolygonVertex({
153+
afterId: button.afterId,
154+
polygonStartIndex: button.polygonStartIndex,
155+
polygonLength: button.polygonLength,
156+
x: coordToInt(button.lat),
157+
y: coordToInt(button.lon),
158+
}),
159+
)
160+
}}
161+
/>
162+
))}
163+
85164
{/* Group fencePolygonItems into separate polygons */}
86165
{(() => {
87-
const polygons = []
88-
let currentPolygon = []
89-
let currentPoints = 0
90-
91-
fencePolygonItems.forEach((item) => {
92-
currentPolygon.push(item)
93-
currentPoints++
94-
95-
if (currentPoints === item.param1) {
96-
polygons.push(currentPolygon)
97-
currentPolygon = []
98-
currentPoints = 0
99-
}
100-
})
101-
102-
return polygons.map((polygon, index) => {
103-
const lastPolygonItem = polygon[polygon.length - 1]
166+
return fencePolygonGroups.map((polygon, index) => {
167+
const lastPolygonItem = polygon.items[polygon.items.length - 1]
104168

105169
const color =
106170
lastPolygonItem.command === 5002
@@ -111,11 +175,14 @@ export default function FenceItems({ fenceItems }) {
111175
<DrawLineCoordinates
112176
key={index}
113177
coordinates={[
114-
...polygon.map((item) => [
178+
...polygon.items.map((item) => [
115179
intToCoord(item.y),
116180
intToCoord(item.x),
117181
]),
118-
[intToCoord(polygon[0].y), intToCoord(polygon[0].x)],
182+
[
183+
intToCoord(polygon.items[0].y),
184+
intToCoord(polygon.items[0].x),
185+
],
119186
]}
120187
colour={color}
121188
lineProps={{ "line-width": 2, "line-dasharray": [4, 6] }}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
A small button rendered at a map midpoint for inserting a new point.
3+
*/
4+
5+
import { Marker } from "react-map-gl"
6+
7+
export default function MidpointInsertButton({
8+
lat,
9+
lon,
10+
colour,
11+
tooltipText,
12+
onClick,
13+
}) {
14+
return (
15+
<Marker latitude={lat} longitude={lon} offset={[0, 0]}>
16+
<div
17+
onMouseDown={(e) => {
18+
e.preventDefault()
19+
e.stopPropagation()
20+
}}
21+
onClick={(e) => {
22+
e.preventDefault()
23+
e.stopPropagation()
24+
onClick?.()
25+
}}
26+
className="pointer-events-auto"
27+
>
28+
<button
29+
type="button"
30+
title={tooltipText}
31+
aria-label={tooltipText}
32+
className="flex h-4 w-4 items-center justify-center rounded-full border border-black/30 text-[1rem] text-black font-bold"
33+
style={{ backgroundColor: colour }}
34+
>
35+
+
36+
</button>
37+
</div>
38+
</Marker>
39+
)
40+
}

gcs/src/components/mapComponents/missionItems.jsx

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55
connecting them.
66
*/
77
import { useMemo } from "react"
8-
import { useSelector } from "react-redux"
8+
import { useDispatch, useSelector } from "react-redux"
99
import { selectCurrentPage } from "../../redux/slices/droneConnectionSlice"
1010
import { selectHomePosition } from "../../redux/slices/droneInfoSlice"
1111
import {
12+
insertDrawingItemAfter,
1213
selectActiveTab,
1314
selectPlannedHomePosition,
1415
} from "../../redux/slices/missionSlice"
1516

1617
// Helper imports
17-
import { intToCoord } from "../../helpers/dataFormatters"
18+
import { coordToInt, intToCoord } from "../../helpers/dataFormatters"
1819
import { filterMissionItems } from "../../helpers/filterMissions"
1920

2021
// Styling imports
@@ -23,14 +24,17 @@ import "maplibre-gl/dist/maplibre-gl.css"
2324
// Component imports
2425
import DrawLineCoordinates from "./drawLineCoordinates"
2526
import MarkerPin from "./markerPin"
27+
import MidpointInsertButton from "./midpointInsertButton"
2628

2729
// Tailwind styling
30+
import { midpoint, point } from "@turf/turf"
2831
import resolveConfig from "tailwindcss/resolveConfig"
2932
import tailwindConfig from "../../../tailwind.config"
3033

3134
const tailwindColors = resolveConfig(tailwindConfig).theme.colors
3235

3336
export default function MissionItems({ missionItems }) {
37+
const dispatch = useDispatch()
3438
const currentPage = useSelector(selectCurrentPage)
3539
const editable =
3640
useSelector(selectActiveTab) === "mission" && currentPage === "missions"
@@ -49,6 +53,19 @@ export default function MissionItems({ missionItems }) {
4953
[filteredMissionItems],
5054
)
5155

56+
const missionPathItems = useMemo(() => {
57+
if (filteredMissionItems.length === 0) return []
58+
59+
const stopCommandItem = [...missionItems]
60+
.filter((item) => [20, 21, 189].includes(item.command))
61+
.sort((a, b) => a.seq - b.seq)
62+
.at(0)
63+
64+
return stopCommandItem
65+
? filteredMissionItems.filter((item) => item.seq <= stopCommandItem.seq)
66+
: filteredMissionItems
67+
}, [filteredMissionItems, missionItems])
68+
5269
const takeoffWaypoint = useMemo(() => {
5370
return missionItems.find((item) => item.command === 22)
5471
}, [missionItems])
@@ -58,6 +75,40 @@ export default function MissionItems({ missionItems }) {
5875
[filteredMissionItems, homePosition, takeoffWaypoint],
5976
)
6077

78+
const insertionMidpoints = useMemo(() => {
79+
if (!editable || missionPathItems.length < 2) return []
80+
81+
return missionPathItems
82+
.slice(0, -1)
83+
.map((startItem, index) => {
84+
const endItem = missionPathItems[index + 1]
85+
86+
const hasHiddenMissionItemsBetween = missionItems.some((item) => {
87+
if (item.seq <= startItem.seq || item.seq >= endItem.seq) {
88+
return false
89+
}
90+
91+
const itemIsRenderedOnMap =
92+
item.x !== 0 && item.y !== 0 && item.command !== 20
93+
return !itemIsRenderedOnMap
94+
})
95+
96+
if (hasHiddenMissionItemsBetween) {
97+
return null
98+
}
99+
100+
const midpointCoords = getMidpointCoordinates(startItem, endItem)
101+
102+
return {
103+
afterId: startItem.id,
104+
lat: midpointCoords[1],
105+
lon: midpointCoords[0],
106+
tooltipText: `Insert waypoint between ${startItem.seq} and ${endItem.seq}`,
107+
}
108+
})
109+
.filter(Boolean)
110+
}, [editable, missionPathItems])
111+
61112
function getListOfLineCoordinates(filteredMissionItems) {
62113
if (filteredMissionItems.length === 0) return { solid: [], dotted: [] }
63114

@@ -152,6 +203,13 @@ export default function MissionItems({ missionItems }) {
152203
return { solid: lineCoordsList, dotted: dottedLineSegmentsList }
153204
}
154205

206+
function getMidpointCoordinates(startItem, endItem) {
207+
return midpoint(
208+
point([intToCoord(startItem.y), intToCoord(startItem.x)]),
209+
point([intToCoord(endItem.y), intToCoord(endItem.x)]),
210+
).geometry.coordinates
211+
}
212+
155213
return (
156214
<>
157215
{/* Show mission item LABELS */}
@@ -170,6 +228,31 @@ export default function MissionItems({ missionItems }) {
170228
)
171229
})}
172230

231+
{insertionMidpoints.map((midpointItem) => (
232+
<MidpointInsertButton
233+
key={midpointItem.afterId}
234+
lat={midpointItem.lat}
235+
lon={midpointItem.lon}
236+
colour={tailwindColors.yellow[400]}
237+
tooltipText={midpointItem.tooltipText}
238+
onClick={() => {
239+
const afterItem = missionPathItems.find(
240+
(item) => item.id === midpointItem.afterId,
241+
)
242+
243+
if (!afterItem) return
244+
245+
dispatch(
246+
insertDrawingItemAfter({
247+
afterId: afterItem.id,
248+
x: coordToInt(midpointItem.lat),
249+
y: coordToInt(midpointItem.lon),
250+
}),
251+
)
252+
}}
253+
/>
254+
))}
255+
173256
{/* Show mission item outlines */}
174257
<DrawLineCoordinates
175258
coordinates={listOfLineCoords}

0 commit comments

Comments
 (0)