diff --git a/gcs/src/components/missions/missionStatistics.jsx b/gcs/src/components/missions/missionStatistics.jsx new file mode 100644 index 000000000..95ecd10bb --- /dev/null +++ b/gcs/src/components/missions/missionStatistics.jsx @@ -0,0 +1,222 @@ +/* + The MissionStatistics component calculates and displays various statistics + about the mission on the missions screen. +*/ + +import { Tooltip } from "@mantine/core" +import { distance } from "@turf/turf" +import { useEffect, useState } from "react" +import { intToCoord } from "../../helpers/dataFormatters" +import { + filterMissionItems, + isGlobalFrameHomeCommand, +} from "../../helpers/filterMissions" + +function calculateMaxAltitude(missionItems) { + missionItems = missionItems.filter( + (item) => isGlobalFrameHomeCommand(item) === false, + ) + + return Math.max(...missionItems.map((item) => item.z || 0), 0) +} + +function calculateMaxDistanceBetweenWaypoints(missionItems) { + missionItems = missionItems.filter( + (item) => isGlobalFrameHomeCommand(item) === false, + ) + if (missionItems.length < 2) return 0 + let maxDistance = 0 + let maxDistancePoints = [] + + // Remove any waypoints without coordinates + missionItems = missionItems.filter((item) => item.x !== 0 && item.y !== 0) + + for (let i = 0; i < missionItems.length - 1; i++) { + const item1 = missionItems[i] + const item2 = missionItems[i + 1] + const distanceBetweenPoints = distance( + [intToCoord(item1.y), intToCoord(item1.x)], + [intToCoord(item2.y), intToCoord(item2.x)], + { + units: "meters", + }, + ) + if (distanceBetweenPoints > maxDistance) { + maxDistance = distanceBetweenPoints + maxDistancePoints = [item1, item2] + } + } + + // distance to 2dp + maxDistance = Math.round(maxDistance * 100) / 100 + + return { maxDistance: maxDistance, points: maxDistancePoints } +} + +function calculateMaxSlopeGradient(missionItems) { + const homeCommand = isGlobalFrameHomeCommand(missionItems[0]) + ? missionItems[0] + : null + if (homeCommand) { + missionItems = missionItems.slice(1) // Remove home command if it exists + } + + if (missionItems.length < 2) return 0 + let maxGradient = 0 + let maxDistancePoints = [] + + // If the first command is a takeoff command, use the coordinates from the home location + if (homeCommand && missionItems[0].command === 22) { + missionItems[0].x = homeCommand.x + missionItems[0].y = homeCommand.y + } + + for (let i = 0; i < missionItems.length - 1; i++) { + const item1 = missionItems[i] + const item2 = missionItems[i + 1] + + if (item1.z === undefined || item2.z === undefined) continue + + const verticalDistance = Math.abs(item2.z - item1.z) + const horizontalDistance = distance( + [intToCoord(item1.y), intToCoord(item1.x)], + [intToCoord(item2.y), intToCoord(item2.x)], + { units: "meters" }, + ) + + if (horizontalDistance === 0) continue // Avoid division by zero + + const gradient = (verticalDistance / horizontalDistance) * 100 // Convert to percentage + + if (gradient > maxGradient) { + maxGradient = gradient + maxDistancePoints = [item1, item2] + } + } + + maxGradient = Math.round(maxGradient * 100) / 100 // Round to two decimal places + + return { maxGradient: maxGradient, points: maxDistancePoints } +} + +function calculateTotalDistance(missionItems) { + // This function should calculate the total distance of the waypoints, + // ignoring any waypoints without coordinates. If the jump command (177) is present, + // then the distance for all of the jump waypoints for the number of laps should + // be calculated as well. + let totalDistance = 0 + let lastPoint = null + + for (let i = 0; i < missionItems.length; i++) { + const item = missionItems[i] + + if (item.command === 177) { + const jumpTo = item.param1 + const jumpCount = item.param2 + + // Find the waypoint with the seq value equal to jumpTo + const jumpWaypoint = missionItems.find((wp) => wp.seq === jumpTo) + + if (jumpWaypoint) { + // Calculate the distance from the jumpWaypoint to the current waypoint + // times the number of jumps + if (lastPoint) { + totalDistance += + distance( + [intToCoord(lastPoint.y), intToCoord(lastPoint.x)], + [intToCoord(jumpWaypoint.y), intToCoord(jumpWaypoint.x)], + { units: "meters" }, + ) * jumpCount + } + + for (let j = 0; j < jumpCount; j++) { + // Slice the missionItems list between the actual array index of jumpWaypoint and i + const jumpStartIdx = missionItems.findIndex((wp) => wp.seq === jumpTo) + const jumpItems = missionItems.slice(jumpStartIdx, i) + totalDistance += calculateTotalDistance(jumpItems) + } + } + } + + if (item.x === 0 || item.y === 0) continue // Skip waypoints without coordinates + + if (lastPoint) { + totalDistance += distance( + [intToCoord(lastPoint.y), intToCoord(lastPoint.x)], + [intToCoord(item.y), intToCoord(item.x)], + { units: "meters" }, + ) + } + lastPoint = item + } + + return Math.round(totalDistance * 100) / 100 +} + +function StatisticItem({ label, value, units, tooltip = null }) { + const displayString = `${label}: ${value}${units || ""}` + return ( + <> + {tooltip ? ( + +

{displayString}

+
+ ) : ( +

{displayString}

+ )} + + ) +} + +export default function MissionStatistics({ missionItems }) { + const [filteredMissionItems, setFilteredMissionItems] = useState([]) + const [totalDistance, setTotalDistance] = useState(0) + const [maxDistanceBetweenWaypoints, setMaxDistanceBetweenWaypoints] = + useState({ maxDistance: 0, points: null }) + const [maxAltitude, setMaxAltitude] = useState(0) + const [maxSlopeGradient, setMaxSlopeGradient] = useState({ + maxGradient: 0, + points: null, + }) + + useEffect(() => { + setFilteredMissionItems(filterMissionItems(missionItems)) + }, [missionItems]) + + useEffect(() => { + if (filteredMissionItems.length === 0) return + + // Use unfiltered mission items + setTotalDistance(calculateTotalDistance(missionItems)) + setMaxAltitude(calculateMaxAltitude(filteredMissionItems)) + setMaxDistanceBetweenWaypoints( + calculateMaxDistanceBetweenWaypoints(filteredMissionItems), + ) + setMaxSlopeGradient(calculateMaxSlopeGradient(filteredMissionItems)) + }, [filteredMissionItems]) + + return ( + <> + + + + + + ) +} diff --git a/gcs/src/helpers/filterMissions.js b/gcs/src/helpers/filterMissions.js index 1c21df4bd..18198a5f9 100644 --- a/gcs/src/helpers/filterMissions.js +++ b/gcs/src/helpers/filterMissions.js @@ -1,4 +1,7 @@ -import { FILTER_MISSION_ITEM_COMMANDS_LIST } from "./mavlinkConstants" +import { + FILTER_MISSION_ITEM_COMMANDS_LIST, + MAV_FRAME_LIST, +} from "./mavlinkConstants" export function filterMissionItems(missionItems) { const filteredMissionItems = [] @@ -23,3 +26,17 @@ export function filterMissionItems(missionItems) { return filteredMissionItems } + +export function isGlobalFrameHomeCommand(waypoint) { + const globalFrameValue = parseInt( + Object.keys(MAV_FRAME_LIST).find( + (key) => MAV_FRAME_LIST[key] === "MAV_FRAME_GLOBAL", + ), + ) + return ( + waypoint.frame === globalFrameValue && + waypoint.x !== 0 && + waypoint.y !== 0 && + waypoint.command === 16 + ) +} diff --git a/gcs/src/missions.jsx b/gcs/src/missions.jsx index 2f2f8066e..e2939dcd2 100644 --- a/gcs/src/missions.jsx +++ b/gcs/src/missions.jsx @@ -28,10 +28,12 @@ import { IconInfoCircle } from "@tabler/icons-react" import Layout from "./components/layout" import FenceItemsTable from "./components/missions/fenceItemsTable" import MissionItemsTable from "./components/missions/missionItemsTable" +import MissionStatistics from "./components/missions/missionStatistics" import MissionsMapSection from "./components/missions/missionsMap" import RallyItemsTable from "./components/missions/rallyItemsTable" import NoDroneConnected from "./components/noDroneConnected" import { coordToInt, intToCoord } from "./helpers/dataFormatters" +import { isGlobalFrameHomeCommand } from "./helpers/filterMissions" import { COPTER_MODES_FLIGHT_MODE_MAP, MAV_AUTOPILOT_INVALID, @@ -264,20 +266,6 @@ export default function Missions() { }) } - function isGlobalFrameHomeCommand(waypoint) { - const globalFrameValue = parseInt( - Object.keys(MAV_FRAME_LIST).find( - (key) => MAV_FRAME_LIST[key] === "MAV_FRAME_GLOBAL", - ), - ) - return ( - waypoint.frame === globalFrameValue && - waypoint.x !== 0 && - waypoint.y !== 0 && - waypoint.command === 16 - ) - } - function updateHomePositionBasedOnWaypoints(waypoints) { if (waypoints.length > 0) { const potentialHomeLocation = waypoints[0] @@ -786,6 +774,12 @@ export default function Missions() { )}

+ + + +
+ +