From 2bdb7e78c478522e32f5e80834368d1dfe3257c6 Mon Sep 17 00:00:00 2001 From: Kilian Hofmann Date: Thu, 17 Jul 2025 20:57:12 +0200 Subject: [PATCH] Last leg types --- README.md | 17 ++++-- browser/src/parser/parser.ts | 36 +++++++++-- browser/src/parser/terminators/FC.ts | 2 +- browser/src/parser/terminators/HA.ts | 88 +++++++++++++++++++++++++++ browser/src/parser/terminators/HF.ts | 88 +++++++++++++++++++++++++++ browser/src/parser/terminators/HM.ts | 88 +++++++++++++++++++++++++++ browser/src/parser/terminators/PI.ts | 76 +++++++++++++++++++++++ browser/src/types/terminators/HA.d.ts | 6 ++ browser/src/types/terminators/HF.d.ts | 6 ++ browser/src/types/terminators/HM.d.ts | 6 ++ browser/src/types/terminators/PI.d.ts | 19 ++++++ 11 files changed, 419 insertions(+), 13 deletions(-) create mode 100644 browser/src/parser/terminators/HA.ts create mode 100644 browser/src/parser/terminators/HF.ts create mode 100644 browser/src/parser/terminators/HM.ts create mode 100644 browser/src/parser/terminators/PI.ts create mode 100644 browser/src/types/terminators/HA.d.ts create mode 100644 browser/src/types/terminators/HF.d.ts create mode 100644 browser/src/types/terminators/HM.d.ts create mode 100644 browser/src/types/terminators/PI.d.ts diff --git a/README.md b/README.md index ce04131..e6a23c8 100644 --- a/README.md +++ b/README.md @@ -346,7 +346,7 @@ LFPV 27 PB2V SID (Cycle 2507, ID 10395) - `NavDist` is in **nmi**. `NavDist` is geodesic. -## Holding mandatory (HA) +## Holding mandatory (altitude end) (HA) ### Example @@ -420,7 +420,7 @@ My guess is that `Distance` always is a distance. This would match with the exam The Leg terminates after the hold is exited ath the hold entry fix. -## Holding mandatory (HM) +## Holding mandatory (manual end) (HM) ### Example @@ -493,8 +493,11 @@ FAUP VDM35 APP (Cycle 2507, ID 67790) ### Instructions - From the fix identified by (`WptID`, `WptLat`, `WptLon`) *or* - (`NavID`, `NavLat`, `NavLon`, `NavDist`, `NavBear`), fly `Course` for `Distance`. -- Turn `TurnDir` until aligned with succeeding leg course. + (`NavID`, `NavLat`, `NavLon`, `NavDist`, `NavBear`), fly direct to succeeding leg fix. +- Turn `TurnDir` onto `Course`. +- Fly for 1 minute. +- Turn 180°. +- Fly until intercepting course of succeeding leg. - `Alt` shall be the altitude during the procedure. ### Units @@ -505,8 +508,10 @@ FAUP VDM35 APP (Cycle 2507, ID 67790) ### Notes -This needs to look into the succeeding leg in order to calculate the intercept point. -This intercept point then becomes the origin fix of the succeeding leg. +This needs to look into the succeeding leg in order to fly the first segment and get the course +to intercept after the reversal. +The database does not specify how long the outbound leg shall be. +1 minute is assumed here (googled it, seemed a common one). ## Radius to Fix (RF) diff --git a/browser/src/parser/parser.ts b/browser/src/parser/parser.ts index 194d4c5..fe1b7de 100644 --- a/browser/src/parser/parser.ts +++ b/browser/src/parser/parser.ts @@ -11,7 +11,11 @@ import { TerminatorsFA } from './terminators/FA'; import { TerminatorsFC } from './terminators/FC'; import { TerminatorsFD } from './terminators/FD'; import { TerminatorsFM } from './terminators/FM'; +import { TerminatorsHA } from './terminators/HA'; +import { TerminatorsHF } from './terminators/HF'; +import { TerminatorsHM } from './terminators/HM'; import { TerminatorsIF } from './terminators/IF'; +import { TerminatorsPI } from './terminators/PI'; import { TerminatorsRF } from './terminators/RF'; import { TerminatorsTF } from './terminators/TF'; import { TerminatorsVA } from './terminators/VA'; @@ -135,7 +139,10 @@ class Parser { // Main for (let index = 0; index < procedure.length; index++) { + console.log('Processing leg with ID', procedure[index].ID); + const leg = procedure[index]; + leg.Alt = leg.IsMAP ? String(runway.Elevation + 200).padStart(5, '0') : leg.Alt; const previousFix = navFixes.at(-1)!; legOptions.isMAP ||= previousFix.IsMAP ?? false; const waypoint = this.waypoints.find(({ ID }) => ID === leg.WptID); @@ -208,11 +215,21 @@ class Parser { navFixes.at(-1)!.isFlyOver = true; break; } - case 'HA': - case 'HF': - case 'HM': - console.error('Unknown TrackCode', leg.TrackCode); + case 'HA': { + const [fixToAdd, lineToAdd] = TerminatorsHA(leg as HATerminalEntry, previousFix); + update(fixToAdd, lineToAdd, { ...legOptions }); break; + } + case 'HF': { + const [fixToAdd, lineToAdd] = TerminatorsHF(leg as HFTerminalEntry, previousFix); + update(fixToAdd, lineToAdd, { ...legOptions }); + break; + } + case 'HM': { + const [fixToAdd, lineToAdd] = TerminatorsHM(leg as HMTerminalEntry, previousFix); + update(fixToAdd, lineToAdd, { ...legOptions }); + break; + } case 'IF': { const fixToAdd = TerminatorsIF(leg as RFTerminalEntry, waypoint); // Only Runway, replace @@ -222,9 +239,16 @@ class Parser { } break; } - case 'PI': - console.error('Unknown TrackCode', leg.TrackCode); + case 'PI': { + const nextLeg = procedure[index + 1] as CFTerminalEntry; + const _waypoint = this.waypoints.find(({ ID }) => ID === nextLeg.WptID); + const [fixToAdd, lineToAdd] = TerminatorsPI(leg as PITerminalEntry, nextLeg, previousFix, lastCourse, [ + waypoint, + _waypoint, + ]); + update(fixToAdd, lineToAdd, { ...legOptions }); break; + } case 'RF': { const [fixToAdd, lineToAdd] = TerminatorsRF(leg as RFTerminalEntry, previousFix, lastCourse, waypoint); if (fixToAdd) { diff --git a/browser/src/parser/terminators/FC.ts b/browser/src/parser/terminators/FC.ts index 87c8088..dc624b7 100644 --- a/browser/src/parser/terminators/FC.ts +++ b/browser/src/parser/terminators/FC.ts @@ -80,7 +80,7 @@ export const TerminatorsFC = ( } const targetFix: NavFix = { - ...computeDestinationPoint(arcEnd, leg.Distance.toMetre(), lastCourse), + ...computeDestinationPoint(arcEnd, leg.Distance.toMetre(), leg.Course.toTrue(refFix)), name: leg.Distance.toString(), isFlyOver: true, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, diff --git a/browser/src/parser/terminators/HA.ts b/browser/src/parser/terminators/HA.ts new file mode 100644 index 0000000..c700344 --- /dev/null +++ b/browser/src/parser/terminators/HA.ts @@ -0,0 +1,88 @@ +import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; +import Parser from '../parser'; +import { computeSpeed } from '../utils/computeSpeed'; +import { computeTurnRate } from '../utils/computeTurnRate'; + +// NOTE: Distance is interpreted as distance and not time +export const TerminatorsHA = (leg: HATerminalEntry, previousFix: NavFix): [NavFix?, LineSegment[]?] => { + const refFix = { + latitude: leg.WptLat, + longitude: leg.WptLon, + }; + const speed = computeSpeed(leg, previousFix); + const turnRate = computeTurnRate(speed, Parser.AC_BANK); + const inboundCrs = leg.Course.toTrue(refFix); + const outboundCrs = inboundCrs.reciprocalCourse(); + + const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; + + // Generate top arc + let currentCrs = inboundCrs; + while (!currentCrs.equal(outboundCrs)) { + let time = 0; + if (leg.TurnDir === 'R') { + const delta = (outboundCrs - currentCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (currentCrs - outboundCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + currentCrs + ); + + line.push([arcFix.longitude, arcFix.latitude]); + } + + const outboundStart = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + leg.Distance.toMetre(), + outboundCrs + ); + line.push([outboundStart.longitude, outboundStart.latitude]); + + // Generate bottom arc + currentCrs = outboundCrs; + while (!currentCrs.equal(inboundCrs)) { + let time = 0; + if (leg.TurnDir === 'R') { + const delta = (inboundCrs - currentCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (currentCrs - inboundCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + currentCrs + ); + + line.push([arcFix.longitude, arcFix.latitude]); + } + + line.push([refFix.longitude, refFix.latitude]); + + return [refFix, line]; +}; diff --git a/browser/src/parser/terminators/HF.ts b/browser/src/parser/terminators/HF.ts new file mode 100644 index 0000000..937793d --- /dev/null +++ b/browser/src/parser/terminators/HF.ts @@ -0,0 +1,88 @@ +import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; +import Parser from '../parser'; +import { computeSpeed } from '../utils/computeSpeed'; +import { computeTurnRate } from '../utils/computeTurnRate'; + +// NOTE: Distance is interpreted as distance and not time +export const TerminatorsHF = (leg: HFTerminalEntry, previousFix: NavFix): [NavFix?, LineSegment[]?] => { + const refFix = { + latitude: leg.WptLat, + longitude: leg.WptLon, + }; + const speed = computeSpeed(leg, previousFix); + const turnRate = computeTurnRate(speed, Parser.AC_BANK); + const inboundCrs = leg.Course.toTrue(refFix); + const outboundCrs = inboundCrs.reciprocalCourse(); + + const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; + + // Generate top arc + let currentCrs = inboundCrs; + while (!currentCrs.equal(outboundCrs)) { + let time = 0; + if (leg.TurnDir === 'R') { + const delta = (outboundCrs - currentCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (currentCrs - outboundCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + currentCrs + ); + + line.push([arcFix.longitude, arcFix.latitude]); + } + + const outboundStart = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + leg.Distance.toMetre(), + outboundCrs + ); + line.push([outboundStart.longitude, outboundStart.latitude]); + + // Generate bottom arc + currentCrs = outboundCrs; + while (!currentCrs.equal(inboundCrs)) { + let time = 0; + if (leg.TurnDir === 'R') { + const delta = (inboundCrs - currentCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (currentCrs - inboundCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + currentCrs + ); + + line.push([arcFix.longitude, arcFix.latitude]); + } + + line.push([refFix.longitude, refFix.latitude]); + + return [refFix, line]; +}; diff --git a/browser/src/parser/terminators/HM.ts b/browser/src/parser/terminators/HM.ts new file mode 100644 index 0000000..48ff3f7 --- /dev/null +++ b/browser/src/parser/terminators/HM.ts @@ -0,0 +1,88 @@ +import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; +import Parser from '../parser'; +import { computeSpeed } from '../utils/computeSpeed'; +import { computeTurnRate } from '../utils/computeTurnRate'; + +// NOTE: Distance is interpreted as distance and not time +export const TerminatorsHM = (leg: HMTerminalEntry, previousFix: NavFix): [NavFix?, LineSegment[]?] => { + const refFix = { + latitude: leg.WptLat, + longitude: leg.WptLon, + }; + const speed = computeSpeed(leg, previousFix); + const turnRate = computeTurnRate(speed, Parser.AC_BANK); + const inboundCrs = leg.Course.toTrue(refFix); + const outboundCrs = inboundCrs.reciprocalCourse(); + + const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; + + // Generate top arc + let currentCrs = inboundCrs; + while (!currentCrs.equal(outboundCrs)) { + let time = 0; + if (leg.TurnDir === 'R') { + const delta = (outboundCrs - currentCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (currentCrs - outboundCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + currentCrs + ); + + line.push([arcFix.longitude, arcFix.latitude]); + } + + const outboundStart = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + leg.Distance.toMetre(), + outboundCrs + ); + line.push([outboundStart.longitude, outboundStart.latitude]); + + // Generate bottom arc + currentCrs = outboundCrs; + while (!currentCrs.equal(inboundCrs)) { + let time = 0; + if (leg.TurnDir === 'R') { + const delta = (inboundCrs - currentCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (currentCrs - inboundCrs).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + currentCrs = (currentCrs - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + currentCrs + ); + + line.push([arcFix.longitude, arcFix.latitude]); + } + + line.push([refFix.longitude, refFix.latitude]); + + return [refFix, line]; +}; diff --git a/browser/src/parser/terminators/PI.ts b/browser/src/parser/terminators/PI.ts new file mode 100644 index 0000000..4cf5c07 --- /dev/null +++ b/browser/src/parser/terminators/PI.ts @@ -0,0 +1,76 @@ +import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; +import Parser from '../parser'; +import { generatePerformanceArc } from '../pathGenerators/generatePerformanceArc'; +import { computeIntersection } from '../utils/computeIntersection'; +import { computeSpeed } from '../utils/computeSpeed'; +import { computeTurnRate } from '../utils/computeTurnRate'; + +export const TerminatorsPI = ( + leg: PITerminalEntry, + nextLeg: CFTerminalEntry, // As per NG docs in the TrmLegTypes.json + previousFix: NavFix, + lastCourse: number, + waypoints: [Waypoint?, Waypoint?] +): [NavFix?, LineSegment[]?] => { + const speed = computeSpeed(leg, previousFix); + const turnRate = computeTurnRate(speed, Parser.AC_BANK); + + const originFix: NavFix = { + latitude: leg.WptLat, + longitude: leg.WptLon, + name: waypoints[0]?.Ident ?? undefined, + isFlyOver: leg.IsFlyOver, + altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, + speed: speed, + speedConstraint: leg.SpeedLimit, + altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, + }; + const endFix: NavFix = { + latitude: nextLeg.WptLat, + longitude: nextLeg.WptLon, + name: waypoints[1]?.Ident ?? undefined, + isFlyOver: leg.IsFlyOver, + altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, + speed: speed, + speedConstraint: leg.SpeedLimit, + altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, + }; + const outboundCrs = leg.Course.toTrue(endFix); + const interceptCrs = nextLeg.Course.toTrue(endFix); + + const line: LineSegment[] = [ + [originFix.longitude, originFix.latitude], + [endFix.longitude, endFix.latitude], + ]; + + // Outbound end + const outEnd = computeDestinationPoint( + endFix, + ((speed / 3600) * 60).toMetre(), // 1min leg + outboundCrs + ); + line.push([outEnd.longitude, outEnd.latitude]); + + // Arc + line.push(...generatePerformanceArc(outboundCrs.reciprocalCourse(), outboundCrs, outEnd, speed, leg.TurnDir)); + + // Intercept + const interceptFix = computeIntersection( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + outboundCrs.reciprocalCourse(), + endFix, + interceptCrs.reciprocalCourse() + ); + + line.push([interceptFix!.longitude, interceptFix!.latitude]); + line.push([endFix!.longitude, endFix!.latitude]); + + return [undefined, line]; +}; diff --git a/browser/src/types/terminators/HA.d.ts b/browser/src/types/terminators/HA.d.ts new file mode 100644 index 0000000..c7f0d4e --- /dev/null +++ b/browser/src/types/terminators/HA.d.ts @@ -0,0 +1,6 @@ +export declare global { + type HATerminalEntry = Required< + Pick + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/HF.d.ts b/browser/src/types/terminators/HF.d.ts new file mode 100644 index 0000000..7895532 --- /dev/null +++ b/browser/src/types/terminators/HF.d.ts @@ -0,0 +1,6 @@ +export declare global { + type HFTerminalEntry = Required< + Pick + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/HM.d.ts b/browser/src/types/terminators/HM.d.ts new file mode 100644 index 0000000..41b4f32 --- /dev/null +++ b/browser/src/types/terminators/HM.d.ts @@ -0,0 +1,6 @@ +export declare global { + type HMTerminalEntry = Required< + Pick + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/PI.d.ts b/browser/src/types/terminators/PI.d.ts new file mode 100644 index 0000000..f20abf2 --- /dev/null +++ b/browser/src/types/terminators/PI.d.ts @@ -0,0 +1,19 @@ +export declare global { + type PITerminalEntry = Required< + Pick< + TerminalEntry, + | 'WptID' + | 'WptLat' + | 'WptLon' + | 'NavID' + | 'NavLat' + | 'NavLon' + | 'NavBear' + | 'NavDist' + | 'Course' + | 'Distance' + | 'Alt' + > + > & + TerminalEntry; +}