UNPKG

flight-planner

Version:
355 lines (354 loc) 14.8 kB
import { createWaypoint, waypointDistance, waypointHeading } from './waypoint.js'; import { calculateGroundspeed, calculateWindCorrectionAngle, calculateWindVector } from './utils.js'; import { bearingToAzimuth, lineString, point as turfPoint, pointToLineDistance } from '@turf/turf'; import { InsufficientWaypointsError } from './exceptions.js'; /** * Checks if a given track is eastbound (0-179 degrees). * * @param {number} track - The track in degrees. * @returns {boolean} True if the track is eastbound, false otherwise. */ export const isEastbound = (track) => { const normalizedTrack = bearingToAzimuth(track); return normalizedTrack >= 0 && normalizedTrack <= 179; }; /** * Checks if a given track is westbound (180-359 degrees). * * @param {number} track - The track in degrees. * @returns {boolean} True if the track is westbound, false otherwise. */ export const isWestbound = (track) => { return !isEastbound(track); }; /** * Calculates the appropriate VFR cruising altitude based on the track and desired minimum altitude. * * Eastbound flights (0-179 degrees) use odd thousands + 500 feet (e.g., 3500, 5500). * Westbound flights (180-359 degrees) use even thousands + 500 feet (e.g., 4500, 6500). * The function returns the lowest VFR cruising altitude that is at or above the given minimum altitude. * * @param {number} track - The true track in degrees. * @param {number} altitude - The minimum desired altitude in feet. * @returns {number} The calculated VFR cruising altitude in feet. */ export function calculateVFRCruisingAltitude(track, altitude) { let altitudeLevel; if (isEastbound(track)) { altitudeLevel = 3500; while (altitudeLevel < altitude) { altitudeLevel += 2000; } } else { altitudeLevel = 4500; while (altitudeLevel < altitude) { altitudeLevel += 2000; } } return altitudeLevel; } /** * Converts an altitude in feet to the corresponding flight level. * Flight levels are expressed in hundreds of feet, so the function divides the altitude by 100. * * @param {number} altitude - The altitude in feet. * @returns {number} The flight level (FL) corresponding to the given altitude. */ export const flightLevel = (altitude) => { return Math.floor(altitude / 100); }; /** * Finds the closest route leg to a given location. * * @param routeTrip - The route trip to search within. * @param location - The location to find the closest leg to, as a [longitude, latitude] tuple. * @returns The closest route leg, or undefined if no route legs are found or the input is invalid. */ export function closestRouteLeg(routeTrip, location) { if (!routeTrip || !routeTrip.route || routeTrip.route.length === 0) { return undefined; } const targetPointFeature = turfPoint(location); let minDistanceToLeg = Infinity; let closestLeg = undefined; for (const leg of routeTrip.route) { const startWp = leg.start.waypoint; const endWp = leg.end.waypoint; if (!startWp.coords || !endWp.coords) { continue; } const legLine = lineString([startWp.coords, endWp.coords]); const distance = pointToLineDistance(targetPointFeature, legLine, { units: 'nauticalmiles' }); if (distance < minDistanceToLeg) { minDistanceToLeg = distance; closestLeg = leg; } } return closestLeg; } /** * Finds the closest waypoint in a route trip to a given location. * * @param routeTrip - The route trip to search within. * @param location - The location to find the closest waypoint to, as a [longitude, latitude] tuple. * @returns The closest waypoint, or undefined if no waypoints are found or the input is invalid. */ export function closestWaypoint(routeTrip, location) { if (!routeTrip || !routeTrip.route || routeTrip.route.length === 0) { return undefined; } const currentLocationAsWaypoint = createWaypoint(location, 'currentLocationPoint'); let minDistance = Infinity; let closestWaypoint = undefined; const uniqueWaypoints = routeTripWaypoints(routeTrip); for (const wp of uniqueWaypoints) { if (!wp.coords) { continue; } const dist = waypointDistance(currentLocationAsWaypoint, wp); if (dist < minDistance) { minDistance = dist; closestWaypoint = wp; } } return closestWaypoint; } /** * Maps a route trip to an array of unique waypoints. * * This function extracts all waypoints from a route trip by taking the start and end * waypoints of each leg and removing duplicates. * * @param routeTrip - The route trip containing legs with start and end waypoints * @returns An array of unique waypoints representing all points in the route trip */ export function routeTripWaypoints(routeTrip) { const allWaypoints = routeTrip.route.flatMap(leg => [leg.start.waypoint, leg.end.waypoint]); const uniqueWaypoints = new Map(); for (const waypoint of allWaypoints) { uniqueWaypoints.set(waypoint.name, waypoint); } return Array.from(uniqueWaypoints.values()); } /** * Converts an array of waypoints to an array of route segments. * * This is a convenience function for preparing waypoints to be used with `calculateNavLog`. * Each waypoint is converted to a segment with an optional altitude. * * @param waypoints - Array of waypoints to convert to segments * @param altitude - Optional altitude in feet to apply to all segments * @returns Array of route segments ready for navigation log calculation * * @example * ```typescript * const waypoints = [departureAerodrome, enrouteWaypoint, arrivalAerodrome]; * const segments = waypointsToSegments(waypoints, 5500); * const navLog = calculateNavLog({ segments, aircraft }); * ``` */ export function waypointsToSegments(waypoints, altitude) { return waypoints.map(waypoint => ({ waypoint, ...(altitude !== undefined && { altitude }) })); } /** * Gets the departure waypoint from a route trip. * * @param routeTrip - The route trip from which to extract the departure waypoint * @returns The departure waypoint, which is the first waypoint in the route */ export const routeTripDepartureWaypoint = (routeTrip) => { return routeTrip.route[0].start.waypoint; }; /** * Gets the arrival waypoint from a route trip. * * @param routeTrip - The route trip from which to extract the arrival waypoint * @returns The arrival waypoint, which is the last waypoint in the route */ export const routeTripArrivalWaypoint = (routeTrip) => { return routeTrip.route[routeTrip.route.length - 1].end.waypoint; }; /** * Calculates a detailed navigation log (nav log) based on the provided options. * * A navigation log contains comprehensive flight performance calculations for each leg of the route, * including wind corrections, heading calculations, groundspeed, fuel consumption, and timing. * This is the detailed computational output that pilots use for in-flight navigation, as opposed * to a flight plan which is the route description submitted to ATC. * * The function performs the following calculations: * - Route leg analysis: distance, true/magnetic tracks, and headings * - Wind correction angles and ground speeds based on forecast winds * - Fuel planning: trip fuel, reserves, taxi, takeoff, landing, and alternate fuel * - Timing: leg durations, departure/arrival times * - Performance metrics: true airspeed, headwind/crosswind components * * @param options - Navigation log options containing segments, aircraft, and fuel parameters. * @returns A RouteTrip object containing the complete navigation log with all calculated performance data. * @throws {InsufficientWaypointsError} If fewer than 2 waypoints are provided in segments. * * @example * ```typescript * const navLog = calculateNavLog({ * segments: [ * { waypoint: departureAerodrome }, * { waypoint: enrouteWaypoint, altitude: 5500 }, * { waypoint: arrivalAerodrome } * ], * aircraft: { cruiseSpeed: 120, fuelConsumption: 8.5 }, * altitude: 5500, * reserveFuelDuration: 45 * }); * ``` */ export function calculateNavLog(options) { const { segments: inputSegments, alternateSegment: inputAlternateSegment, aircraft, departureDate = new Date(), altitude, reserveFuelDuration = 30, reserveFuel, taxiFuel, takeoffFuel, landingFuel } = options; if (inputSegments.length < 2) { throw new InsufficientWaypointsError(inputSegments.length); } const segments = inputSegments.map(seg => ({ ...seg })); const alternateSegment = inputAlternateSegment ? { ...inputAlternateSegment } : undefined; if (segments[0].waypoint.elevation !== undefined) { segments[0].altitude = segments[0].waypoint.elevation; } if (segments[segments.length - 1].waypoint.elevation !== undefined) { segments[segments.length - 1].altitude = segments[segments.length - 1].waypoint.elevation; } if (altitude) { segments.forEach((segment, i) => { if (i !== 0 && i !== segments.length - 1 && !segment.altitude) { segment.altitude = altitude; } }); } const aircraftPerformance = aircraft?.cruiseSpeed ? { cruiseSpeed: aircraft.cruiseSpeed, fuelConsumption: aircraft.fuelConsumption } : undefined; const lastLegIndex = segments.length - 2; const legs = segments.slice(0, -1).map((startSegment, i) => calculateRouteLeg(startSegment, segments[i + 1], aircraftPerformance, i === lastLegIndex)); let routeAlternate; if (alternateSegment) { if (altitude && !alternateSegment.altitude) { alternateSegment.altitude = altitude; } routeAlternate = calculateRouteLeg(segments[segments.length - 1], alternateSegment, aircraftPerformance, true); } let totalDistance = 0; let totalDuration = 0; let totalFuelConsumption = 0; for (const leg of legs) { totalDistance += leg.course.distance; totalDuration += leg.performance?.duration || 0; totalFuelConsumption += leg.performance?.fuelConsumption || 0; const legArrivalDate = leg.performance && new Date(departureDate.getTime() + totalDuration * 60 * 1000); leg.arrivalDate = legArrivalDate; } const reserveFuelRequired = reserveFuel ?? (aircraft?.fuelConsumption ? aircraft.fuelConsumption * (reserveFuelDuration / 60) : 0); const alternateFuel = routeAlternate?.performance?.fuelConsumption || 0; const totalTripFuel = totalFuelConsumption + (reserveFuelRequired || 0) + (takeoffFuel || 0) + (landingFuel || 0) + (taxiFuel || 0) + alternateFuel; const fuelBreakdown = { trip: totalFuelConsumption, reserve: reserveFuelRequired, takeoff: takeoffFuel !== undefined ? takeoffFuel : undefined, landing: landingFuel !== undefined ? landingFuel : undefined, taxi: taxiFuel !== undefined ? taxiFuel : undefined, alternate: routeAlternate?.performance?.fuelConsumption !== undefined ? routeAlternate.performance.fuelConsumption : undefined }; return { route: legs, routeAlternate, totalDistance, totalDuration, totalTripFuel, fuelBreakdown, departureDate, arrivalDate: new Date(departureDate.getTime() + totalDuration * 60 * 1000), generatedAt: new Date(), }; } /** * Calculates a single leg of a flight route. * * @param {RouteSegment} start - The starting segment of the leg. * @param {RouteSegment} end - The ending segment of the leg. * @param {AircraftPerformance} [aircraft] - The aircraft performance data for calculations. * @param {boolean} [isLastLeg=false] - Indicates whether this is the last leg of the route; when true, the end waypoint's wind data is used instead of the start waypoint's. * @returns {RouteLeg} The calculated route leg. */ function calculateRouteLeg(start, end, aircraft, isLastLeg = false) { const course = calculateRouteCourse(start.waypoint, end.waypoint); // Use start waypoint wind for most legs; use end waypoint wind for the // last leg so arrival wind checks reflect the destination weather. const wind = isLastLeg ? end.waypoint.metarStation?.metar.wind : start.waypoint.metarStation?.metar.wind; const performance = aircraft && wind ? calculatePerformance(aircraft, course, wind) : undefined; return { start, end, course, wind, performance, }; } /** * Calculates the course vector (distance and track) between two waypoints. * * @param {Waypoint} start - The starting waypoint. * @param {Waypoint} end - The ending waypoint. * @returns {CourseVector} The calculated course vector. */ function calculateRouteCourse(start, end) { const trueTrack = waypointHeading(start, end); const magneticDeclination = start.declination || end.declination || 0; return { distance: waypointDistance(start, end), track: bearingToAzimuth(trueTrack), magneticTrack: bearingToAzimuth(trueTrack - magneticDeclination), }; } /** * Calculates aircraft performance for a given course, aircraft performance data, and wind conditions. * * @param {AircraftPerformance} aircraft - The aircraft performance data (cruise speed and fuel consumption). * @param {CourseVector} course - The course vector (track and distance). * @param {Wind} wind - The wind conditions. * @returns {RouteLegPerformance} The calculated aircraft performance. */ function calculatePerformance(aircraft, course, wind) { // Calculate the true airspeed const trueAirspeed = aircraft.cruiseSpeed; // Calculate wind correction angle and magnetic heading const wca = calculateWindCorrectionAngle(wind, course.track, trueAirspeed); const trueHeading = bearingToAzimuth(course.track + wca); const magneticHeading = bearingToAzimuth(course.magneticTrack + wca); // Groundspeed calculation uses true heading. const groundSpeed = calculateGroundspeed(wind, trueAirspeed, trueHeading); const duration = groundSpeed > 0 ? (course.distance / groundSpeed) * 60 : 0; const fuelConsumption = aircraft.fuelConsumption && (aircraft.fuelConsumption * (duration / 60)); // Calculate wind vector components const windVector = calculateWindVector(wind, course.track); return { headWind: windVector.headwind, crossWind: windVector.crosswind, trueAirspeed, windCorrectionAngle: wca, trueHeading, magneticHeading, groundSpeed, duration, fuelConsumption }; }