UNPKG

@fboes/aerofly-patterns

Version:

Landegerät - Create random custom missions for Aerofly FS 4.

180 lines (156 loc) 6.51 kB
import { Point, Vector } from "@fboes/geojson"; import { Rand } from "../general/Rand.js"; import { Configuration } from "./Configuration.js"; import { Units } from "../../data/Units.js"; import { AeroflyAircraft } from "../../data/AeroflyAircraft.js"; import { Degree, degreeDifference } from "../general/Degree.js"; import { HoldingPatternFix } from "./HoldingPatternFix.js"; export type HoldingPatternEntry = "direct" | "parallel" | "offset"; /** * Represents a holding pattern for an aircraft. * This class encapsulates the properties and calculations needed to define a holding pattern, * including inbound heading, turn direction, DME distance, pattern altitude, and leg time. * * @see https://www.faa.gov/air_traffic/publications/atpubs/aip_html/part2_enr_section_1.5.html */ export class HoldingPattern { id: string; inboundHeading: number; inboundHeadingTrue: number; isLeftTurn: boolean; /** * In nautical miles * This is the distance from the holding fix to the DME fix. */ dmeDistanceNm: number; /** * In nautical miles */ dmeDistanceOutboundNm: number; /** * Indicates if the DME procedure is flown towards the VOR (false) or away from it (true). */ dmeHoldingAwayFromNavaid: boolean; /** * The direction of the holding pattern. */ holdingAreaDirection: number; holdingAreaDirectionTrue: number; /** * Pattern altitude in feet MSL. * This is the altitude at which the aircraft should hold. */ patternAltitudeFt: number; patternSpeedKts: number; /** * In minutes * This is the time the aircraft should hold on each leg of the holding pattern. * It is used to calculate the distance flown during the holding pattern. */ legTimeMin: number; /** * The fix around which the holding pattern is built. * This is the Navaid that the aircraft will hold at, or in case of a DME procedure, the DME fix. */ holdingFix: Point; /** * The turn radius of the holding pattern in meters. */ turnRadiusMeters: number; /** * The distance of the inbound leg in the holding pattern in meters. */ legDistanceMeters: number; furtherClearanceInMin: number; constructor(configuration: Configuration, holdingNavAid: HoldingPatternFix, aircraft: AeroflyAircraft) { this.inboundHeading = configuration.inboundHeading === -1 ? Rand.getRandomInt(0, 359) : configuration.inboundHeading; this.inboundHeadingTrue = this.inboundHeading + holdingNavAid.mag_dec; this.isLeftTurn = Math.random() < configuration.leftHandPatternProbability; this.dmeDistanceNm = Math.random() < configuration.dmeProcedureProbability && ["VORTAC", "VOR/DME"].includes(holdingNavAid.type) ? Rand.getRandomInt(configuration.minimumDmeDistance, configuration.maximumDmeDistance) : 0; this.dmeHoldingAwayFromNavaid = this.dmeDistanceNm > 0 && Math.random() <= configuration.dmeHoldingAwayFromNavaidProbability; this.dmeDistanceOutboundNm = this.dmeDistanceNm > 0 ? this.dmeDistanceNm + (this.dmeHoldingAwayFromNavaid ? -4 : 4) : 0; this.patternAltitudeFt = Math.round(Rand.getRandomInt(configuration.minimumHoldingAltitude, configuration.maximumHoldingAltitude) / 100) * 100; this.patternSpeedKts = Math.min( aircraft.cruiseSpeedKts + 10, this.#getMaxPatternSpeedKts(aircraft, this.patternAltitudeFt), ); this.legTimeMin = this.#getLegTimeMin(this.patternAltitudeFt); this.id = this.dmeDistanceNm <= 0 ? holdingNavAid.id : `${holdingNavAid.id}+${String(this.dmeDistanceNm).padStart(2, "0")}`; this.holdingFix = this.#getHoldingFix(holdingNavAid); this.turnRadiusMeters = this.#getTurnRadiusMeters(this.patternSpeedKts); this.legDistanceMeters = this.#getLegDistanceMeters(this.patternSpeedKts, this.legTimeMin); this.holdingAreaDirection = Degree(this.inboundHeading + (this.dmeHoldingAwayFromNavaid ? 0 : 180)); this.holdingAreaDirectionTrue = Degree(this.holdingAreaDirection + holdingNavAid.mag_dec); this.furtherClearanceInMin = Rand.getRandomInt(3, 5) * 5; //console.log(this); } #getHoldingFix(holdingNavAid: HoldingPatternFix): Point { return holdingNavAid.position.getPointBy( new Vector(this.dmeDistanceNm * Units.metersPerNauticalMile, Degree(this.inboundHeadingTrue + 180)), ); } /** * @see https://www.code7700.com/holding.htm */ #getMaxPatternSpeedKts(aircraft: AeroflyAircraft, patternAltitudeFt: number): number { // TODO: Turbulence: 280 if (aircraft.tags.includes("helicopter")) { return patternAltitudeFt <= 6000 ? 100 : 170; } if (patternAltitudeFt <= 6000) { return 200; // FAA } if (patternAltitudeFt <= 14000) { return 230; // ICAO / FAA } if (patternAltitudeFt <= 20000) { return 240; // ICAO } return 265; // ICAO } /** * @see https://skybrary.aero/articles/holding-pattern * During entry and holding, pilots manually flying the aircraft are expected * to make all turns to achieve an average bank angle of at least 25˚ or * a rate of turn of 3˚ per second, whichever requires the lesser bank. */ #getTurnRadiusMeters(patternSpeedKts: number): number { return (patternSpeedKts / (20 * Math.PI * 3)) * Units.metersPerNauticalMile; // turn radius at 3 degrees per second //return (patternSpeedKts ** 2 / (11.26 * Math.tan(25 * (Math.PI / 180)))) * Units.metersPerNauticalMile; // turn radius at 25 degrees bank angle } #getLegDistanceMeters(patternSpeedKts: number, legTimeMin: number): number { if (this.dmeDistanceOutboundNm !== 0) { return Math.abs( Math.sqrt((this.dmeDistanceOutboundNm * Units.metersPerNauticalMile) ** 2 - (this.turnRadiusMeters * 2) ** 2) - this.dmeDistanceNm * Units.metersPerNauticalMile, ); } return (patternSpeedKts / 60) * legTimeMin * Units.metersPerNauticalMile; } #getLegTimeMin(patternAltitudeFt: number): number { return patternAltitudeFt > 14000 ? 1.5 : 1; } getEntry(bearing: number): HoldingPatternEntry { const delta = degreeDifference(this.holdingAreaDirectionTrue, bearing) * (this.isLeftTurn ? -1 : 1); if (delta >= 110) { return "offset"; } if (delta <= -70) { return "parallel"; } return "direct"; } getFurtherClearance(date: Date): Date { const furtherClearanceInMs = this.furtherClearanceInMin * 60 * 1000; return new Date(date.getTime() + furtherClearanceInMs); } }