From b11e2a22f8b3af7300245b7c86fb09d297ce3cbe Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Sun, 10 Aug 2025 21:15:17 +0100 Subject: [PATCH 1/3] Add mission statistics --- .../components/missions/missionStatistics.jsx | 220 ++++++++++++++++++ gcs/src/missions.jsx | 35 +-- 2 files changed, 241 insertions(+), 14 deletions(-) create mode 100644 gcs/src/components/missions/missionStatistics.jsx diff --git a/gcs/src/components/missions/missionStatistics.jsx b/gcs/src/components/missions/missionStatistics.jsx new file mode 100644 index 000000000..242b5b3c4 --- /dev/null +++ b/gcs/src/components/missions/missionStatistics.jsx @@ -0,0 +1,220 @@ +/* + 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 } from "../../helpers/filterMissions" +import { isGlobalFrameHomeCommand } from "../../missions" + +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 (117) 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 jumpTo.seq and i + const jumpItems = missionItems.slice(jumpWaypoint.seq, i) + console.log(jumpItems, calculateTotalDistance(jumpItems)) + 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 + } + + // Round to two decimal places and convert to km + return Math.round(totalDistance * 100) / 100 / 1000 +} + +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 + + setTotalDistance(calculateTotalDistance(missionItems)) + setMaxAltitude(calculateMaxAltitude(filteredMissionItems)) + setMaxDistanceBetweenWaypoints( + calculateMaxDistanceBetweenWaypoints(filteredMissionItems), + ) + setMaxSlopeGradient(calculateMaxSlopeGradient(filteredMissionItems)) + }, [filteredMissionItems]) + + return ( + <> + + + + + + ) +} diff --git a/gcs/src/missions.jsx b/gcs/src/missions.jsx index fd0b1181f..cfc76d14a 100644 --- a/gcs/src/missions.jsx +++ b/gcs/src/missions.jsx @@ -27,6 +27,7 @@ import { import { IconInfoCircle } from "@tabler/icons-react" import Layout from "./components/layout" 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" @@ -45,6 +46,20 @@ import { socket } from "./helpers/socket" const coordsFractionDigits = 7 +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 + ) +} + export default function Missions() { // Local Storage const [connected] = useSessionStorage({ @@ -248,20 +263,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] @@ -716,6 +717,12 @@ export default function Missions() { )}

+ + + +
+ +
From 7568873cc25b405301572c4cbc433d7a6bfbb923 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Sun, 10 Aug 2025 21:27:14 +0100 Subject: [PATCH 2/3] Address copilot review comments --- gcs/src/components/missions/missionStatistics.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gcs/src/components/missions/missionStatistics.jsx b/gcs/src/components/missions/missionStatistics.jsx index 242b5b3c4..72c2ea733 100644 --- a/gcs/src/components/missions/missionStatistics.jsx +++ b/gcs/src/components/missions/missionStatistics.jsx @@ -99,7 +99,7 @@ function calculateMaxSlopeGradient(missionItems) { function calculateTotalDistance(missionItems) { // This function should calculate the total distance of the waypoints, - // ignoring any waypoints without coordinates. If the jump command (117) is present, + // 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 @@ -128,9 +128,9 @@ function calculateTotalDistance(missionItems) { } for (let j = 0; j < jumpCount; j++) { - // Slice the missionItems list between jumpTo.seq and i - const jumpItems = missionItems.slice(jumpWaypoint.seq, i) - console.log(jumpItems, calculateTotalDistance(jumpItems)) + // 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) } } @@ -148,8 +148,7 @@ function calculateTotalDistance(missionItems) { lastPoint = item } - // Round to two decimal places and convert to km - return Math.round(totalDistance * 100) / 100 / 1000 + return Math.round(totalDistance * 100) / 100 } function StatisticItem({ label, value, units, tooltip = null }) { @@ -185,6 +184,7 @@ export default function MissionStatistics({ missionItems }) { useEffect(() => { if (filteredMissionItems.length === 0) return + // Use unfiltered mission items setTotalDistance(calculateTotalDistance(missionItems)) setMaxAltitude(calculateMaxAltitude(filteredMissionItems)) setMaxDistanceBetweenWaypoints( @@ -195,7 +195,7 @@ export default function MissionStatistics({ missionItems }) { return ( <> - + Date: Sat, 16 Aug 2025 22:01:57 +0100 Subject: [PATCH 3/3] Move command into filterMissions file --- .../components/missions/missionStatistics.jsx | 6 ++++-- gcs/src/helpers/filterMissions.js | 19 ++++++++++++++++++- gcs/src/missions.jsx | 15 +-------------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/gcs/src/components/missions/missionStatistics.jsx b/gcs/src/components/missions/missionStatistics.jsx index 72c2ea733..95ecd10bb 100644 --- a/gcs/src/components/missions/missionStatistics.jsx +++ b/gcs/src/components/missions/missionStatistics.jsx @@ -7,8 +7,10 @@ import { Tooltip } from "@mantine/core" import { distance } from "@turf/turf" import { useEffect, useState } from "react" import { intToCoord } from "../../helpers/dataFormatters" -import { filterMissionItems } from "../../helpers/filterMissions" -import { isGlobalFrameHomeCommand } from "../../missions" +import { + filterMissionItems, + isGlobalFrameHomeCommand, +} from "../../helpers/filterMissions" function calculateMaxAltitude(missionItems) { missionItems = missionItems.filter( 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 cfc76d14a..16edc9e08 100644 --- a/gcs/src/missions.jsx +++ b/gcs/src/missions.jsx @@ -32,6 +32,7 @@ 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, @@ -46,20 +47,6 @@ import { socket } from "./helpers/socket" const coordsFractionDigits = 7 -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 - ) -} - export default function Missions() { // Local Storage const [connected] = useSessionStorage({