diff --git a/gcs/src/components/mapComponents/contextMenuItem.jsx b/gcs/src/components/mapComponents/contextMenuItem.jsx index 970d8c1b3..03fa547ac 100644 --- a/gcs/src/components/mapComponents/contextMenuItem.jsx +++ b/gcs/src/components/mapComponents/contextMenuItem.jsx @@ -4,7 +4,7 @@ export default function ContextMenuItem({ children, onClick }) { className="hover:bg-falcongrey-800 hover:cursor-pointer py-1 px-4 rounded" onClick={onClick} > - {children} +
{children}
) } diff --git a/gcs/src/components/mapComponents/contextMenuSubMenuItem.jsx b/gcs/src/components/mapComponents/contextMenuSubMenuItem.jsx new file mode 100644 index 000000000..5f29e2eca --- /dev/null +++ b/gcs/src/components/mapComponents/contextMenuSubMenuItem.jsx @@ -0,0 +1,54 @@ +import { useEffect, useRef, useState } from "react" + +export default function ContextMenuSubMenuItem({ children, title }) { + const [isHovered, setIsHovered] = useState(false) + const [submenuPosition, setSubmenuPosition] = useState({ top: 0, left: 0 }) + const parentRef = useRef(null) + const subMenuRef = useRef(null) + + useEffect(() => { + if (isHovered && parentRef.current && subMenuRef.current) { + const parentRect = parentRef.current.getBoundingClientRect() + const submenuRect = subMenuRef.current.getBoundingClientRect() + const viewportWidth = window.innerWidth + + // Calculate whether to position the submenu to the right or left + const shouldAppearToLeft = + parentRect.right + submenuRect.width > viewportWidth + + setSubmenuPosition({ + top: parentRect.top, + left: shouldAppearToLeft + ? parentRect.left - submenuRect.width + : parentRect.right, + }) + } + }, [isHovered]) + + return ( +
setIsHovered(true)} + onMouseOut={() => setIsHovered(false)} + > +
+

{title}

+ +
+ {isHovered && ( +
+ {children} +
+ )} +
+ ) +} diff --git a/gcs/src/components/mapComponents/drawLineCoordinates.jsx b/gcs/src/components/mapComponents/drawLineCoordinates.jsx index 28e72176a..1f9f75aa8 100644 --- a/gcs/src/components/mapComponents/drawLineCoordinates.jsx +++ b/gcs/src/components/mapComponents/drawLineCoordinates.jsx @@ -9,6 +9,8 @@ export default function DrawLineCoordinates({ colour, width = 1, lineProps = {}, + fillLayer = false, + fillOpacity = 0.5, }) { return ( + {fillLayer && ( + + )} ) } diff --git a/gcs/src/components/mapComponents/fenceItems.jsx b/gcs/src/components/mapComponents/fenceItems.jsx new file mode 100644 index 000000000..795a35b1e --- /dev/null +++ b/gcs/src/components/mapComponents/fenceItems.jsx @@ -0,0 +1,181 @@ +/* + The FenceItems component returns the markers and lines connecting the + markers together on the map to display. It also filters out any + items which should not be displayed on the map as markers or not have lines + connecting them. It properly parses the type of fence marker. +*/ + +import { useEffect, useState } from "react" + +// Helper imports +import { intToCoord } from "../../helpers/dataFormatters" + +// Styling imports +import "maplibre-gl/dist/maplibre-gl.css" + +// Component imports + +// Tailing styling +import { circle } from "@turf/turf" +import { Layer, Source } from "react-map-gl" +import resolveConfig from "tailwindcss/resolveConfig" +import tailwindConfig from "../../../tailwind.config" +import { FENCE_ITEM_COMMANDS_LIST } from "../../helpers/mavlinkConstants" +import DrawLineCoordinates from "./drawLineCoordinates" +import MarkerPin from "./markerPin" +const tailwindColors = resolveConfig(tailwindConfig).theme.colors + +function getFenceCommandNumber(value) { + return parseInt( + Object.keys(FENCE_ITEM_COMMANDS_LIST).filter( + (key) => FENCE_ITEM_COMMANDS_LIST[key] === value, + ), + ) +} + +const polygonCommands = [ + getFenceCommandNumber("MAV_CMD_NAV_FENCE_POLYGON_VERTEX_INCLUSION"), + getFenceCommandNumber("MAV_CMD_NAV_FENCE_POLYGON_VERTEX_EXCLUSION"), +] +const circleCommands = [ + getFenceCommandNumber("MAV_CMD_NAV_FENCE_CIRCLE_INCLUSION"), + getFenceCommandNumber("MAV_CMD_NAV_FENCE_CIRCLE_EXCLUSION"), +] + +export default function FenceItems({ + fenceItems, + editable = false, + dragEndCallback = () => {}, +}) { + const [fencePolygonItems, setFencePolygonItems] = useState([]) + const [fenceCircleItems, setFenceCircleItems] = useState([]) + + 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), + ) + + setFencePolygonItems(polygonItems) + setFenceCircleItems(circleItems) + }, [fenceItems]) + + return ( + <> + {/* Show mission geo-fence MARKERS */} + {fencePolygonItems.map((item, index) => { + return ( + + ) + })} + + {/* 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] + + const color = + lastPolygonItem.command === 5002 + ? tailwindColors.red[500] + : tailwindColors.blue[200] + + return ( + [ + intToCoord(item.y), + intToCoord(item.x), + ]), + [intToCoord(polygon[0].y), intToCoord(polygon[0].x)], + ]} + colour={color} + lineProps={{ "line-width": 2, "line-dasharray": [4, 6] }} + fillLayer={true} + fillOpacity={lastPolygonItem.command === 5002 ? 0.2 : 0} + /> + ) + }) + })()} + + {fenceCircleItems.map((item, index) => { + return ( + + ) + })} + + + circle([intToCoord(item.y), intToCoord(item.x)], item.param1, { + steps: 64, // Number of points to create the circle + units: "meters", // Units for the radius + properties: { + color: + item.command === 5004 + ? tailwindColors.red[500] + : tailwindColors.blue[200], + fillOpacity: item.command === 5004 ? 0.2 : 0, // No fill if inclusion + }, + }), + ), + }} + > + + + + + ) +} diff --git a/gcs/src/components/mapComponents/polygon.jsx b/gcs/src/components/mapComponents/polygon.jsx new file mode 100644 index 000000000..d4f87302f --- /dev/null +++ b/gcs/src/components/mapComponents/polygon.jsx @@ -0,0 +1,54 @@ +/* + The PolygonItems component returns the markers and lines connecting the + polygon together on the map to display. +*/ + +// Helper imports + +// Styling imports +import "maplibre-gl/dist/maplibre-gl.css" + +// Component imports +import DrawLineCoordinates from "./drawLineCoordinates" +import MarkerPin from "./markerPin" + +// Tailing styling +import resolveConfig from "tailwindcss/resolveConfig" +import tailwindConfig from "../../../tailwind.config" +const tailwindColors = resolveConfig(tailwindConfig).theme.colors + +export default function Polygon({ + polygonPoints, + editable = false, + dragEndCallback = () => {}, +}) { + return ( + <> + {/* Show polygon MARKERS */} + {polygonPoints.map((item, index) => { + return ( + + ) + })} + + {/* Show polygon outlines */} + {polygonPoints.length > 0 && ( + [item.lon, item.lat]), + [polygonPoints[0].lon, polygonPoints[0].lat], + ]} + colour={tailwindColors.red[200]} + /> + )} + + ) +} diff --git a/gcs/src/components/missions/fenceItemsTable.jsx b/gcs/src/components/missions/fenceItemsTable.jsx new file mode 100644 index 000000000..0037890af --- /dev/null +++ b/gcs/src/components/missions/fenceItemsTable.jsx @@ -0,0 +1,55 @@ +/* + This table displays all the fence items. +*/ + +import { Table } from "@mantine/core" +import React from "react" +import FenceItemsTableRow from "./fenceItemsTableRow" + +function FenceItemsTableNonMemo({ + fenceItems, + updateMissionItem, + deleteMissionItem, + updateMissionItemOrder, +}) { + return ( + + + + + Command + Param 1 + + + + Lat + Lng + + Frame + + + + + {fenceItems.map((fenceItem, idx) => { + return ( + + ) + })} + +
+ ) +} + +function propsAreEqual(prev, next) { + return JSON.stringify(prev) === JSON.stringify(next) +} +const FenceItemsTable = React.memo(FenceItemsTableNonMemo, propsAreEqual) + +export default FenceItemsTable diff --git a/gcs/src/components/missions/fenceItemsTableRow.jsx b/gcs/src/components/missions/fenceItemsTableRow.jsx new file mode 100644 index 000000000..5ba378300 --- /dev/null +++ b/gcs/src/components/missions/fenceItemsTableRow.jsx @@ -0,0 +1,142 @@ +/* + This component displays the row for a fence item in a table. +*/ + +import { + ActionIcon, + NumberInput, + Select, + TableTd, + TableTr, +} from "@mantine/core" +import { IconArrowDown, IconArrowUp, IconTrash } from "@tabler/icons-react" +import { useEffect, useState } from "react" +import { coordToInt, intToCoord } from "../../helpers/dataFormatters" +import { + FENCE_ITEM_COMMANDS_LIST, + MAV_FRAME_LIST, +} from "../../helpers/mavlinkConstants" + +const coordsFractionDigits = 9 + +function getDisplayCommandName(commandName) { + if (commandName.startsWith("MAV_CMD_NAV_")) { + commandName = commandName.replace("MAV_CMD_NAV_", "") + } else if (commandName.startsWith("MAV_CMD_")) { + commandName = commandName.replace("MAV_CMD_", "") + } + + return commandName +} + +function getAvailableCommands() { + var commandsList = FENCE_ITEM_COMMANDS_LIST + + return Object.entries(commandsList).map(([key, value]) => ({ + value: key, + label: getDisplayCommandName(value), + })) +} + +function getFrameName(frameId) { + var frameName = MAV_FRAME_LIST[frameId] + + if (frameName.startsWith("MAV_FRAME_")) { + frameName = frameName.replace("MAV_FRAME_", "") + } + + return frameName || "UNKNOWN" +} + +export default function FenceItemsTableRow({ + index, + fenceItem, + updateMissionItem, + deleteMissionItem, + updateMissionItemOrder, +}) { + const [fenceItemData, setFenceItemData] = useState(fenceItem) + + useEffect(() => { + setFenceItemData(fenceItem) + }, [fenceItem]) + + useEffect(() => { + updateMissionItem(fenceItemData) + }, [fenceItemData]) + + function updateFenceItemData(key, newVal) { + setFenceItemData({ + ...fenceItemData, + [key]: newVal, + }) + } + + return ( + + {index} + +