flight-planner
Version:
Plan and route VFR flights
405 lines (404 loc) • 15.9 kB
JavaScript
// - VFR Minimum Fuel Reserves (SERA.OP.MPA.150/NCO.OP.125-like)
// - Fuel for Alternate Aerodrome
// - Crosswind Limitations
// - Check wind and gust
// - Check runway length
// - Check weight & balance
// - Minimum Safe Altitude (SERA.5005.f / NCO.OP.145): Enroute altitude > 1000 ft above highest obstacle within 500 M
// - Low temparature warning
// - Check for significant weather along route like thunderstorms, icing, turbulence
// - Flight plan for controlled airspace
// - Also check for alternate aerodrome
// - Check for wind gusts
import { routeTripWaypoints } from './planner.js';
import { FlightRules } from './index.js';
import { metarFlightRule, metarCeiling } from './metar.js';
import { isNight } from './sun.js';
import { MAX_LEG_DURATION_MINUTES } from './constants.js';
/**
* Defines the severity level of an advisory.
*/
export var AdvisoryLevel;
(function (AdvisoryLevel) {
AdvisoryLevel["Info"] = "INFO";
AdvisoryLevel["Warning"] = "WARNING";
AdvisoryLevel["Error"] = "ERROR";
})(AdvisoryLevel || (AdvisoryLevel = {}));
/**
* Checks VFR minimum fuel reserves.
* Assumes a 30-minute reserve for VFR day flights.
*
* @param routeTrip The flight plan's route trip.
* @param aircraft The aircraft being used.
* @param options The route options.
* @returns An array of advisories related to fuel reserves.
*/
function checkVfrMinimumFuel(_routeTrip, aircraft, _options) {
const advisories = [];
// const reserveMinutes = options?.reserveFuelDuration ?? 30; // Default to 30 minutes
if (!aircraft.fuelConsumption || aircraft.fuelConsumption <= 0) {
advisories.push({
code: 'ERROR_AIRCRAFT_FUEL_CONSUMPTION_INVALID',
level: AdvisoryLevel.Error,
details: {
fuelConsumption: aircraft.fuelConsumption
},
});
return advisories;
}
// const tripFuel = routeTrip.totalFuelConsumption;
// if (tripFuel === undefined || tripFuel === null) {
// advisories.push({
// code: 'WARN_TRIP_FUEL_MISSING',
// level: AdvisoryLevel.Warning,
// });
// return advisories;
// }
// const reserveFuel = (reserveMinutes / 60) * aircraft.fuelConsumption; // Reserve fuel in liters
// const totalFuelRequired = tripFuel + reserveFuel;
// // Add taxi, takeoff, landing fuel if specified in options
// let contingencyFuel = 0;
// if (options?.taxiFuel) contingencyFuel += options.taxiFuel;
// if (options?.takeoffFuel) contingencyFuel += options.takeoffFuel;
// if (options?.landingFuel) contingencyFuel += options.landingFuel;
// const finalTotalFuelRequired = totalFuelRequired + contingencyFuel;
// if (routeTrip.totalFuelRequired !== undefined && Math.abs(routeTrip.totalFuelRequired - finalTotalFuelRequired) > 0.1) {
// advisories.push({
// code: 'WARN_TOTAL_FUEL_MISMATCH',
// level: AdvisoryLevel.Warning,
// details: {
// routeTripValue: routeTrip.totalFuelRequired,
// calculatedValue: finalTotalFuelRequired
// }
// });
// }
// if (!aircraft.fuelCapacity || aircraft.fuelCapacity <= 0) {
// advisories.push({
// code: 'ERROR_AIRCRAFT_FUEL_CAPACITY_INVALID',
// level: AdvisoryLevel.Error,
// details: {
// fuelCapacity: aircraft.fuelCapacity
// },
// });
// return advisories;
// }
// if (finalTotalFuelRequired > aircraft.fuelCapacity) {
// advisories.push({
// code: 'ERROR_INSUFFICIENT_FUEL_FOR_VFR_RESERVE',
// level: AdvisoryLevel.Error,
// details: {
// requiredFuel: finalTotalFuelRequired,
// tripFuel: tripFuel,
// reserveFuel: reserveFuel,
// contingencyFuel: contingencyFuel,
// aircraftCapacity: aircraft.fuelCapacity,
// reserveDurationMinutes: reserveMinutes,
// },
// });
// }
return advisories;
}
/**
* Checks METAR conditions at all waypoints in a route trip.
* Issues advisories if conditions are IFR or LIFR.
*
* @param routeTrip The flight plan's route trip.
* @returns An array of advisories related to waypoint weather conditions.
*/
function checkVfrMinimumWeatherConditions(routeTrip) {
const advisories = [];
const waypoints = routeTripWaypoints(routeTrip);
for (const waypoint of waypoints) {
const metarStation = waypoint.metarStation;
if (metarStation && metarStation.metar) {
const metar = metarStation.metar;
const flightRule = metarFlightRule(metar);
if (flightRule === FlightRules.IFR || flightRule === FlightRules.LIFR) {
advisories.push({
code: 'ERROR_WEATHER_BELOW_MINIMUM',
level: AdvisoryLevel.Error,
details: {
waypointName: waypoint.name,
station: metarStation.station,
flightRule: flightRule,
visibility: metar.visibility,
ceiling: metarCeiling(metar),
},
});
}
else if (flightRule === FlightRules.MVFR) {
advisories.push({
code: 'WARN_WEATHER_MVFR',
level: AdvisoryLevel.Warning,
details: {
waypointName: waypoint.name,
station: metarStation.station,
flightRule: flightRule,
visibility: metar.visibility,
ceiling: metarCeiling(metar),
},
});
}
}
}
return advisories;
}
/**
* Checks wind conditions (headwind and crosswind) at departure and arrival waypoints.
*
* @param routeTrip The flight plan's route trip.
* @param aircraft The aircraft being used.
* @returns An array of advisories related to wind conditions.
*/
function checkWindLimits(routeTrip, aircraft) {
const advisories = [];
const departureLeg = routeTrip.route[0];
const arrivalLeg = routeTrip.route[routeTrip.route.length - 1];
// --- Check Departure Waypoint ---
const departureWaypoint = departureLeg.start.waypoint;
if (departureLeg.performance) {
const { headWind, crossWind } = departureLeg.performance;
// Crosswind check for departure
if (aircraft.maxDemonstratedCrosswind !== undefined && aircraft.maxDemonstratedCrosswind > 0) {
if (Math.abs(crossWind) > aircraft.maxDemonstratedCrosswind) {
advisories.push({
code: 'ERROR_WIND_DEPARTURE_CROSSWIND_EXCEEDS_LIMITS',
level: AdvisoryLevel.Error,
details: {
waypointName: departureWaypoint.name,
crosswind: crossWind,
maxDemonstratedCrosswind: aircraft.maxDemonstratedCrosswind,
headwind: headWind,
},
});
}
}
// Headwind/Tailwind information for departure
if (headWind < -5) {
advisories.push({
code: 'INFO_WIND_DEPARTURE_SIGNIFICANT_TAILWIND',
level: AdvisoryLevel.Info,
details: {
waypointName: departureWaypoint.name,
tailwind: -headWind,
crosswind: crossWind,
},
});
}
else if (headWind > 15) {
advisories.push({
code: 'INFO_WIND_DEPARTURE_SIGNIFICANT_HEADWIND',
level: AdvisoryLevel.Info,
details: {
waypointName: departureWaypoint.name,
headwind: headWind,
crosswind: crossWind,
},
});
}
}
// --- Check Arrival Waypoint ---
const arrivalWaypoint = arrivalLeg.end.waypoint;
if (arrivalLeg.performance) {
const { headWind, crossWind } = arrivalLeg.performance;
// Crosswind check for arrival
if (aircraft.maxDemonstratedCrosswind !== undefined && aircraft.maxDemonstratedCrosswind > 0) {
if (Math.abs(crossWind) > aircraft.maxDemonstratedCrosswind) {
advisories.push({
code: 'ERROR_WIND_ARRIVAL_CROSSWIND_EXCEEDS_LIMITS',
level: AdvisoryLevel.Error,
details: {
waypointName: arrivalWaypoint.name,
crosswind: crossWind,
maxDemonstratedCrosswind: aircraft.maxDemonstratedCrosswind,
headwind: headWind,
},
});
}
}
// Headwind/Tailwind information for arrival
if (headWind < -5) {
advisories.push({
code: 'INFO_WIND_ARRIVAL_SIGNIFICANT_TAILWIND',
level: AdvisoryLevel.Info,
details: {
waypointName: arrivalWaypoint.name,
tailwind: -headWind,
crosswind: crossWind,
},
});
}
else if (headWind > 15) {
advisories.push({
code: 'INFO_WIND_ARRIVAL_SIGNIFICANT_HEADWIND',
level: AdvisoryLevel.Info,
details: {
waypointName: arrivalWaypoint.name,
headwind: headWind,
crosswind: crossWind,
},
});
}
}
return advisories;
}
/**
* Checks if the planned flight altitude exceeds the aircraft's service ceiling.
*
* @param routeTrip The flight plan's route trip.
* @param aircraft The aircraft being used for the flight.
* @returns An array of advisories.
*/
function checkServiceCeiling(routeTrip, aircraft) {
const advisories = [];
if (aircraft.serviceCeiling === undefined) {
return advisories;
}
if (aircraft.serviceCeiling <= 0) {
advisories.push({
code: 'WARN_AIRCRAFT_SERVICE_CEILING_INVALID',
level: AdvisoryLevel.Warning,
details: {
aircraftRegistration: aircraft.registration,
},
});
return advisories;
}
const routeSegments = routeTrip.route.flatMap(leg => [leg.start, leg.end]);
for (const routeSegment of routeSegments) {
if (routeSegment.altitude !== undefined && routeSegment.altitude > aircraft.serviceCeiling) {
advisories.push({
code: 'ERROR_ALTITUDE_EXCEEDS_SERVICE_CEILING',
level: AdvisoryLevel.Error,
details: {
waypointName: routeSegment.waypoint.name,
altitude: routeSegment.altitude,
serviceCeiling: aircraft.serviceCeiling,
},
});
}
}
return advisories;
}
/**
* Checks if the enroute altitude is above the minimum safe altitude of 500 ft.
*
* @param routeTrip The flight plan's route trip.
* @returns An array of advisories.
*/
function checkMinimumSafeAltitude(routeTrip) {
const advisories = [];
const minimumAltitude = 500;
// Iterate from the second leg to the second-to-last leg
const routeSegments = routeTrip.route.flatMap(leg => [leg.start, leg.end]);
for (let i = 1; i < routeSegments.length - 1; i++) {
const routeSegment = routeSegments[i];
if (routeSegment.altitude !== undefined && routeSegment.altitude < minimumAltitude) {
advisories.push({
code: 'WARN_ALTITUDE_BELOW_MINIMUM_SAFE_ENROUTE',
level: AdvisoryLevel.Warning,
details: {
waypointName: routeSegment.waypoint.name,
altitude: routeSegment.altitude,
minimumSafeAltitude: minimumAltitude,
},
});
}
}
return advisories;
}
function checkNightFlight(routeTrip) {
const advisories = [];
const firstLeg = routeTrip.route[0];
if (firstLeg && routeTrip.departureDate && isNight(firstLeg.start.waypoint, routeTrip.departureDate)) {
advisories.push({
code: 'INFO_NIGHT_DEPARTURE',
level: AdvisoryLevel.Info,
details: {
waypointName: firstLeg.start.waypoint.name,
},
});
}
for (const routeLeg of routeTrip.route) {
if (routeLeg.arrivalDate && isNight(routeLeg.end.waypoint, routeLeg.arrivalDate)) {
advisories.push({
code: 'INFO_NIGHT_WAYPOINT',
level: AdvisoryLevel.Info,
details: {
waypointName: routeLeg.end.waypoint.name,
},
});
}
}
if (routeTrip.routeAlternate && routeTrip.routeAlternate.end && routeTrip.routeAlternate.arrivalDate) {
if (isNight(routeTrip.routeAlternate.end.waypoint, routeTrip.routeAlternate.arrivalDate)) {
advisories.push({
code: 'INFO_NIGHT_ALTERNATE',
level: AdvisoryLevel.Info,
details: {
waypointName: routeTrip.routeAlternate.end.waypoint.name,
},
});
}
}
return advisories;
}
/**
* Checks for excessively long flight legs.
*
* @param routeTrip The flight plan\'s route trip.
* @returns An array of advisories related to long legs.
*/
function checkLongRouteLegs(routeTrip) {
const advisories = [];
for (const leg of routeTrip.route) {
if (leg.performance && leg.performance.duration > MAX_LEG_DURATION_MINUTES) {
advisories.push({
code: 'WARN_LONG_FLIGHT_LEG',
level: AdvisoryLevel.Warning,
details: {
waypointNameStart: leg.start.waypoint.name,
waypointNameEnd: leg.end.waypoint.name,
durationMinutes: leg.performance.duration,
maxRecommendedDurationMinutes: MAX_LEG_DURATION_MINUTES,
},
});
}
}
return advisories;
}
/**
* Validates a RouteTrip against various aviation regulations and best practices.
*
* @param routeTrip The flight plan's route trip.
* @param aircraft The aircraft being used for the flight.
* @param options Optional route configuration.
* @returns An array of advisories.
*/
export function routeTripValidate(routeTrip, aircraft, options) {
let allAdvisories = [];
const fuelAdvisories = checkVfrMinimumFuel(routeTrip, aircraft, options);
allAdvisories = allAdvisories.concat(fuelAdvisories);
const weatherAdvisories = checkVfrMinimumWeatherConditions(routeTrip);
allAdvisories = allAdvisories.concat(weatherAdvisories);
const crosswindAdvisories = checkWindLimits(routeTrip, aircraft);
allAdvisories = allAdvisories.concat(crosswindAdvisories);
const serviceCeilingAdvisories = checkServiceCeiling(routeTrip, aircraft);
allAdvisories = allAdvisories.concat(serviceCeilingAdvisories);
const minSafeAltitudeAdvisories = checkMinimumSafeAltitude(routeTrip);
allAdvisories = allAdvisories.concat(minSafeAltitudeAdvisories);
const nightFlightAdvisories = checkNightFlight(routeTrip);
allAdvisories = allAdvisories.concat(nightFlightAdvisories);
const longLegAdvisories = checkLongRouteLegs(routeTrip);
allAdvisories = allAdvisories.concat(longLegAdvisories);
return allAdvisories;
}
/**
* Checks if advisories contain any errors.
*
* @param advisories An array of advisories to check.
* @returns True if any advisory has an error level, false otherwise.
*/
export function advisoryHasErrors(advisories) {
return advisories.some(advisory => advisory.level === AdvisoryLevel.Error);
}