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({