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
222 changes: 222 additions & 0 deletions gcs/src/components/missions/missionStatistics.jsx
Original file line number Diff line number Diff line change
@@ -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) {
Comment thread
1Blademaster marked this conversation as resolved.
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 ? (
<Tooltip label={tooltip}>
<p>{displayString}</p>
</Tooltip>
) : (
<p>{displayString}</p>
)}
</>
)
}

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))
Comment thread
1Blademaster marked this conversation as resolved.
setMaxAltitude(calculateMaxAltitude(filteredMissionItems))
setMaxDistanceBetweenWaypoints(
calculateMaxDistanceBetweenWaypoints(filteredMissionItems),
)
setMaxSlopeGradient(calculateMaxSlopeGradient(filteredMissionItems))
}, [filteredMissionItems])

return (
<>
<StatisticItem label="Total distance" value={totalDistance} units="m" />
<StatisticItem
label="Max distance between waypoints"
value={maxDistanceBetweenWaypoints.maxDistance}
tooltip={
maxDistanceBetweenWaypoints.points &&
`Between ${maxDistanceBetweenWaypoints.points[0].seq} and ${maxDistanceBetweenWaypoints.points[1].seq}`
}
units="m"
/>
<StatisticItem label="Max altitude" value={maxAltitude} units="m" />
<StatisticItem
label="Max slope gradient"
value={maxSlopeGradient.maxGradient}
tooltip={
maxSlopeGradient.points &&
`Between ${maxSlopeGradient.points[0].seq} and ${maxSlopeGradient.points[1].seq}`
}
units="%"
/>
</>
)
}
19 changes: 18 additions & 1 deletion gcs/src/helpers/filterMissions.js
Original file line number Diff line number Diff line change
@@ -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 = []
Expand All @@ -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
)
}
22 changes: 8 additions & 14 deletions gcs/src/missions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -786,6 +774,12 @@ export default function Missions() {
)}
</p>
</div>

<Divider className="my-1" />

<div className="flex flex-col gap-2">
<MissionStatistics missionItems={missionItems} />
</div>
</div>
</ResizableBox>

Expand Down
Loading