Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 105 additions & 38 deletions gcs/src/components/mapComponents/fenceItems.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,31 @@
connecting them. It properly parses the type of fence marker.
*/

import { useEffect, useState } from "react"
import { useMemo } from "react"

// Helper imports
import { intToCoord } from "../../helpers/dataFormatters"
import { coordToInt, intToCoord } from "../../helpers/dataFormatters"

// Styling imports
import "maplibre-gl/dist/maplibre-gl.css"

// Component imports

// Tailwind styling
import { circle } from "@turf/turf"
import { circle, midpoint, point } from "@turf/turf"
import { Layer, Source } from "react-map-gl"
import { useSelector } from "react-redux"
import { useDispatch, useSelector } from "react-redux"
import resolveConfig from "tailwindcss/resolveConfig"
import tailwindConfig from "../../../tailwind.config"
import { FENCE_ITEM_COMMANDS_LIST } from "../../helpers/mavlinkConstants"
import { selectCurrentPage } from "../../redux/slices/droneConnectionSlice"
import { selectActiveTab } from "../../redux/slices/missionSlice"
import {
insertFencePolygonVertex,
selectActiveTab,
} from "../../redux/slices/missionSlice"
import DrawLineCoordinates from "./drawLineCoordinates"
import MarkerPin from "./markerPin"
import MidpointInsertButton from "./midpointInsertButton"
const tailwindColors = resolveConfig(tailwindConfig).theme.colors

function getFenceCommandNumber(value) {
Expand All @@ -46,33 +50,87 @@ const circleCommands = [
]

export default function FenceItems({ fenceItems }) {
const dispatch = useDispatch()
const currentPage = useSelector(selectCurrentPage)
const editable =
useSelector(selectActiveTab) === "fence" && currentPage === "missions"

const [fencePolygonItems, setFencePolygonItems] = useState([])
const [fenceCircleItems, setFenceCircleItems] = useState([])
const { fencePolygonGroups, fencePolygonItems, fenceCircleItems } =
useMemo(() => {
const polygonItems = []
const circleItems = []
const polygonGroups = []

useEffect(() => {
// Filter out fence items based on their type
const polygonItems = fenceItems.filter((item) =>
polygonCommands.includes(item.command),
)
const circleItems = fenceItems.filter((item) =>
circleCommands.includes(item.command),
)
let currentGroup = []
let currentGroupStartIndex = null

setFencePolygonItems(polygonItems)
setFenceCircleItems(circleItems)
}, [fenceItems])
fenceItems.forEach((item, index) => {
if (polygonCommands.includes(item.command)) {
const polygonItem = { ...item, fenceIndex: index }
polygonItems.push(polygonItem)

if (currentGroup.length === 0) {
currentGroupStartIndex = index
}

currentGroup.push(polygonItem)

if (currentGroup.length === item.param1) {
polygonGroups.push({
items: currentGroup,
startIndex: currentGroupStartIndex,
})
currentGroup = []
currentGroupStartIndex = null
}

return
}

if (circleCommands.includes(item.command)) {
circleItems.push({ ...item, fenceIndex: index })
}
})

return {
fencePolygonGroups: polygonGroups,
fencePolygonItems: polygonItems,
fenceCircleItems: circleItems,
}
}, [fenceItems])

const polygonEdgeInsertButtons = useMemo(() => {
if (!editable) return []

return fencePolygonGroups.flatMap((polygon) => {
if (polygon.items.length < 2) return []

return polygon.items.map((item, index) => {
const nextItem = polygon.items[(index + 1) % polygon.items.length]
const midpointCoords = midpoint(
point([intToCoord(item.y), intToCoord(item.x)]),
point([intToCoord(nextItem.y), intToCoord(nextItem.x)]),
).geometry.coordinates

return {
afterId: item.id,
polygonStartIndex: polygon.startIndex,
polygonLength: polygon.items.length,
lat: midpointCoords[1],
lon: midpointCoords[0],
tooltipText: `Insert vertex between ${item.z + 1} and ${nextItem.z + 1}`,
}
})
})
}, [editable, fencePolygonGroups])

return (
<>
{/* Show mission geo-fence MARKERS */}
{fencePolygonItems.map((item, index) => {
return (
<MarkerPin
key={index}
key={item.id || index}
id={item.id}
lat={intToCoord(item.x)}
lon={intToCoord(item.y)}
Expand All @@ -82,25 +140,31 @@ export default function FenceItems({ fenceItems }) {
)
})}

{polygonEdgeInsertButtons.map((button) => (
<MidpointInsertButton
key={`${button.afterId}:${button.polygonStartIndex}`}
lat={button.lat}
lon={button.lon}
colour={tailwindColors.blue[400]}
tooltipText={button.tooltipText}
onClick={() => {
dispatch(
insertFencePolygonVertex({
afterId: button.afterId,
polygonStartIndex: button.polygonStartIndex,
polygonLength: button.polygonLength,
x: coordToInt(button.lat),
y: coordToInt(button.lon),
}),
)
}}
/>
))}

{/* Group fencePolygonItems into separate polygons */}
{(() => {
const polygons = []
let currentPolygon = []
let currentPoints = 0

fencePolygonItems.forEach((item) => {
currentPolygon.push(item)
currentPoints++

if (currentPoints === item.param1) {
polygons.push(currentPolygon)
currentPolygon = []
currentPoints = 0
}
})

return polygons.map((polygon, index) => {
const lastPolygonItem = polygon[polygon.length - 1]
return fencePolygonGroups.map((polygon, index) => {
const lastPolygonItem = polygon.items[polygon.items.length - 1]

const color =
lastPolygonItem.command === 5002
Expand All @@ -111,11 +175,14 @@ export default function FenceItems({ fenceItems }) {
<DrawLineCoordinates
key={index}
coordinates={[
...polygon.map((item) => [
...polygon.items.map((item) => [
intToCoord(item.y),
intToCoord(item.x),
]),
[intToCoord(polygon[0].y), intToCoord(polygon[0].x)],
[
intToCoord(polygon.items[0].y),
intToCoord(polygon.items[0].x),
],
]}
colour={color}
lineProps={{ "line-width": 2, "line-dasharray": [4, 6] }}
Expand Down
40 changes: 40 additions & 0 deletions gcs/src/components/mapComponents/midpointInsertButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
A small button rendered at a map midpoint for inserting a new point.
*/

import { Marker } from "react-map-gl"

export default function MidpointInsertButton({
lat,
lon,
colour,
tooltipText,
onClick,
}) {
return (
<Marker latitude={lat} longitude={lon} offset={[0, 0]}>
<div
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onClick?.()
}}
className="pointer-events-auto"
>
<button
type="button"
title={tooltipText}
aria-label={tooltipText}
className="flex h-4 w-4 items-center justify-center rounded-full border border-black/30 text-[1rem] text-black font-bold"
style={{ backgroundColor: colour }}
>
+
</button>
</div>
</Marker>
)
}
87 changes: 85 additions & 2 deletions gcs/src/components/mapComponents/missionItems.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
connecting them.
*/
import { useMemo } from "react"
import { useSelector } from "react-redux"
import { useDispatch, useSelector } from "react-redux"
import { selectCurrentPage } from "../../redux/slices/droneConnectionSlice"
import { selectHomePosition } from "../../redux/slices/droneInfoSlice"
import {
insertDrawingItemAfter,
selectActiveTab,
selectPlannedHomePosition,
} from "../../redux/slices/missionSlice"

// Helper imports
import { intToCoord } from "../../helpers/dataFormatters"
import { coordToInt, intToCoord } from "../../helpers/dataFormatters"
import { filterMissionItems } from "../../helpers/filterMissions"

// Styling imports
Expand All @@ -23,14 +24,17 @@ import "maplibre-gl/dist/maplibre-gl.css"
// Component imports
import DrawLineCoordinates from "./drawLineCoordinates"
import MarkerPin from "./markerPin"
import MidpointInsertButton from "./midpointInsertButton"

// Tailwind styling
import { midpoint, point } from "@turf/turf"
import resolveConfig from "tailwindcss/resolveConfig"
import tailwindConfig from "../../../tailwind.config"

const tailwindColors = resolveConfig(tailwindConfig).theme.colors

export default function MissionItems({ missionItems }) {
const dispatch = useDispatch()
const currentPage = useSelector(selectCurrentPage)
const editable =
useSelector(selectActiveTab) === "mission" && currentPage === "missions"
Expand All @@ -49,6 +53,19 @@ export default function MissionItems({ missionItems }) {
[filteredMissionItems],
)

const missionPathItems = useMemo(() => {
if (filteredMissionItems.length === 0) return []

const stopCommandItem = [...missionItems]
.filter((item) => [20, 21, 189].includes(item.command))
.sort((a, b) => a.seq - b.seq)
.at(0)

return stopCommandItem
? filteredMissionItems.filter((item) => item.seq <= stopCommandItem.seq)
: filteredMissionItems
}, [filteredMissionItems, missionItems])

const takeoffWaypoint = useMemo(() => {
return missionItems.find((item) => item.command === 22)
}, [missionItems])
Expand All @@ -58,6 +75,40 @@ export default function MissionItems({ missionItems }) {
[filteredMissionItems, homePosition, takeoffWaypoint],
)

const insertionMidpoints = useMemo(() => {
if (!editable || missionPathItems.length < 2) return []

return missionPathItems
.slice(0, -1)
.map((startItem, index) => {
const endItem = missionPathItems[index + 1]

const hasHiddenMissionItemsBetween = missionItems.some((item) => {
if (item.seq <= startItem.seq || item.seq >= endItem.seq) {
return false
}

const itemIsRenderedOnMap =
item.x !== 0 && item.y !== 0 && item.command !== 20
return !itemIsRenderedOnMap
})

if (hasHiddenMissionItemsBetween) {
return null
}

const midpointCoords = getMidpointCoordinates(startItem, endItem)

return {
afterId: startItem.id,
lat: midpointCoords[1],
lon: midpointCoords[0],
tooltipText: `Insert waypoint between ${startItem.seq} and ${endItem.seq}`,
}
})
.filter(Boolean)
}, [editable, missionPathItems])

function getListOfLineCoordinates(filteredMissionItems) {
if (filteredMissionItems.length === 0) return { solid: [], dotted: [] }

Expand Down Expand Up @@ -152,6 +203,13 @@ export default function MissionItems({ missionItems }) {
return { solid: lineCoordsList, dotted: dottedLineSegmentsList }
}

function getMidpointCoordinates(startItem, endItem) {
return midpoint(
point([intToCoord(startItem.y), intToCoord(startItem.x)]),
point([intToCoord(endItem.y), intToCoord(endItem.x)]),
).geometry.coordinates
}

return (
<>
{/* Show mission item LABELS */}
Expand All @@ -170,6 +228,31 @@ export default function MissionItems({ missionItems }) {
)
})}

{insertionMidpoints.map((midpointItem) => (
<MidpointInsertButton
key={midpointItem.afterId}
lat={midpointItem.lat}
lon={midpointItem.lon}
colour={tailwindColors.yellow[400]}
tooltipText={midpointItem.tooltipText}
onClick={() => {
const afterItem = missionPathItems.find(
(item) => item.id === midpointItem.afterId,
)

if (!afterItem) return

dispatch(
insertDrawingItemAfter({
afterId: afterItem.id,
x: coordToInt(midpointItem.lat),
y: coordToInt(midpointItem.lon),
}),
)
}}
/>
))}

{/* Show mission item outlines */}
<DrawLineCoordinates
coordinates={listOfLineCoords}
Expand Down
Loading
Loading