From 779a78649e0a6bbf771dffc897543e59a5a17199 Mon Sep 17 00:00:00 2001 From: Kilian Hofmann Date: Thu, 17 Jul 2025 03:31:46 +0200 Subject: [PATCH] Improve drawin --- README.md | 42 +++--- browser/src/parser/parser.ts | 25 +++- .../parser/pathGenerators/generateAFArc.ts | 4 +- .../pathGenerators/generatePerformanceArc.ts | 8 +- .../parser/pathGenerators/generateRFArc.ts | 8 +- .../pathGenerators/generateTangentArc.ts | 21 ++- browser/src/parser/terminators/CD.ts | 4 +- browser/src/parser/terminators/CF.ts | 80 +++++++--- browser/src/parser/terminators/CI.ts | 4 +- browser/src/parser/terminators/DF.ts | 103 +++++++++++-- browser/src/parser/terminators/FC.ts | 6 +- browser/src/parser/terminators/FD.ts | 4 +- browser/src/parser/terminators/RF.ts | 1 + browser/src/parser/terminators/TF.ts | 141 ++++++++++++------ browser/src/parser/terminators/VD.ts | 4 +- browser/src/parser/terminators/VI.ts | 6 +- .../src/parser/utils/computeIntersection.ts | 9 +- browser/src/types/navdata.d.ts | 1 + 18 files changed, 340 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index 9dd61f2..10ce689 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ### Example -LGAV BIBE1K SID (Cycle 2507, ID 10653) +LGAV 03L BIBE1K SID (Cycle 2507, ID 10653) ### Minimum Required Fields @@ -40,12 +40,14 @@ LGAV BIBE1K SID (Cycle 2507, ID 10653) While similar to an RF, the center point is coded differently. +Calculate distance for FMS based on ` 2 * π NavDist * abs(NavBear - Course) 360`. + ## Course to Altitude (CA) ### Example -LGAV BIBE1L SID (Cycle 2507, ID 10654) +LGAV 03L BIBE1L SID (Cycle 2507, ID 10654) ### Minimum Required Fields @@ -74,7 +76,7 @@ This new origin is an implicit overfly. ### Example -LGAV BIBE2F SID (Cycle 2507, ID 10657) +LGAV 21L BIBE2F SID (Cycle 2507, ID 10657) ### Minimum Required Fields @@ -105,7 +107,7 @@ This new origin is an implicit overfly. ### Example -LGAV BIBE2F SID (Cycle 2507, ID 10657) +LGAV 21L BIBE2F SID (Cycle 2507, ID 10657) ### Minimum Required Fields @@ -145,7 +147,7 @@ shall be `TurnDir`. ### Example -LGAV BIBE1L SID (Cycle 2507, ID 10654) +LGAV 03L BIBE1L SID (Cycle 2507, ID 10654) ### Minimum Required Fields @@ -170,7 +172,7 @@ This new origin can never be an overfly due to the intercept nature. ### Example -LGAV KOR1D SID (Cycle 2507, ID 10679) +LGAV 03L KOR1D SID (Cycle 2507, ID 10679) ### Minimum Required Fields @@ -197,7 +199,7 @@ This intercept point then becomes the origin fix of the succeeding leg. ### Example -LGAV KOR1D SID (Cycle 2507, ID 10679) +LGAV 03L KOR1D SID (Cycle 2507, ID 10679) ### Minimum Required Fields @@ -219,7 +221,7 @@ LGAV KOR1D SID (Cycle 2507, ID 10679) ### Example -LGAV BIBE2F SID (Cycle 2507, ID 10657) +LGAV 21L BIBE2F SID (Cycle 2507, ID 10657) ### Minimum Required Fields @@ -254,7 +256,7 @@ This new origin is an implicit overfly. ### Example -LIED CAR6F SID (Cycle 2507, ID 11798) +LIED 34L/R CAR6F SID (Cycle 2507, ID 11798) ### Minimum Required Fields @@ -290,7 +292,7 @@ This intercept point then becomes the origin fix of the succeeding leg. ### Example -LGAV BIBE2T SID (Cycle 2507, ID 10659) +LGAV 03R BIBE2T SID (Cycle 2507, ID 10659) ### Minimum Required Fields @@ -325,7 +327,7 @@ This new origin is an implicit overfly. ### Example -LFPV PB2V SID (Cycle 2507, ID 10395) +LFPV 27 PB2V SID (Cycle 2507, ID 10395) ### Minimum Required Fields @@ -457,7 +459,7 @@ My guess as for the missing time/distance decider field, assume it to be distanc ### Example -FAWB VDM29 APP (Cycle 2507, ID 67794), Missed approach procedure +FAWB 29 VDM29 APP (Cycle 2507, ID 67794), Missed approach procedure ### Minimum Required Fields @@ -513,7 +515,7 @@ This intercept point then becomes the origin fix of the succeeding leg. ### Example -LFRN GODA5R SID (cycle 2507, ID 10485) +LFRN 10 GODA5R SID (cycle 2507, ID 10485) ### Minimum Required Fields @@ -545,12 +547,14 @@ No radius is specified, but can be inferred based on center point, both endpoint Example has `NavBear` set to `null`, significance of the inbound tangential track is unknown. +Use `Distance` for calculations in FMS. + ## Track to Fix (TF) ### Example -LFRN GODA5R SID (cycle 2507, ID 10485) +LFRN 10 GODA5R SID (cycle 2507, ID 10485) ### Minimum Required Fields @@ -571,7 +575,7 @@ LFRN GODA5R SID (cycle 2507, ID 10485) ### Example -LFRK LGL4X SID (Cycle 2507, ID 10475) +LFRK 10 LGL4X SID (Cycle 2507, ID 10475) ### Minimum Required Fields @@ -599,7 +603,7 @@ This new origin is an implicit overfly. ### Example -LFRK NEVI4Y SID (Cycle 2507, ID 10482) +LFRK 31 NEVI4Y SID (Cycle 2507, ID 10482) ### Minimum Required Fields @@ -629,7 +633,7 @@ This new origin is an implicit overfly. ### Example -LFRK LUSI4Y SID (Cycle 2507, ID 10480) +LFRK 31 LUSI4Y SID (Cycle 2507, ID 10480) ### Minimum Required Fields @@ -656,7 +660,7 @@ This new origin can never be an overfly due to the intercept nature. ### Example -LFPV PB2P SID (Cycle 2507, ID 10394) +LFPV 27 PB2P SID (Cycle 2507, ID 10394) ### Minimum Required Fields @@ -676,7 +680,7 @@ LFPV PB2P SID (Cycle 2507, ID 10394) ### Example -LIMC MMP8G SID (Cycle 2507, ID 11909) +LIMC 35R MMP8G SID (Cycle 2507, ID 11909) ### Minimum Required Fields diff --git a/browser/src/parser/parser.ts b/browser/src/parser/parser.ts index d7beab5..12f43d9 100644 --- a/browser/src/parser/parser.ts +++ b/browser/src/parser/parser.ts @@ -128,13 +128,16 @@ class Parser { name: runway.Ident, }); let lastCourse = runway.TrueHeading; - const procedure = this._procedures.filter(({ Transition }) => !Transition || Transition === transition); + const procedure = this._procedures.filter( + ({ Transition }) => !Transition || Transition === transition || Transition === 'ALL' + ); // Main for (let index = 0; index < procedure.length; index++) { + //lastCourse = runway.TrueHeading; const leg = procedure[index]; const previousFix = navFixes.at(-1)!; - const waypoint = this.waypoints.filter(({ ID }) => ID === leg.WptID)[0]; + const waypoint = this.waypoints.find(({ ID }) => ID === leg.WptID); switch (leg.TrackCode) { case 'AF': { @@ -200,6 +203,8 @@ class Parser { case 'FM': { const [fixToAdd, lineToAdd] = TerminatorsFM(leg as FMTerminalEntry, previousFix, lastCourse); update(fixToAdd, lineToAdd, { isManual: true }); + // Make overfly + navFixes.at(-1)!.isFlyOver = true; break; } case 'HA': @@ -209,8 +214,11 @@ class Parser { break; case 'IF': { const fixToAdd = TerminatorsIF(leg as RFTerminalEntry, waypoint); - navFixes.length = 0; - navFixes.push(fixToAdd); + // Only Runway, replace + if (navFixes.length <= 1) { + navFixes.push(fixToAdd); + lastCourse = -1; + } break; } case 'PI': @@ -228,7 +236,12 @@ class Parser { break; } case 'TF': { - const [fixToAdd, lineToAdd] = TerminatorsTF(leg as TFTerminalEntry, previousFix, lastCourse, waypoint); + const [fixToAdd, lineToAdd] = TerminatorsTF( + leg as TFTerminalEntry, + { ...previousFix }, // COPY + lastCourse, + waypoint + ); update(fixToAdd, lineToAdd); break; } @@ -255,6 +268,8 @@ class Parser { case 'VM': { const [fixToAdd, lineToAdd] = TerminatorsVM(leg as VMTerminalEntry, previousFix, lastCourse); update(fixToAdd, lineToAdd, { isManual: true }); + // Make overfly + navFixes.at(-1)!.isFlyOver = true; break; } case 'VR': { diff --git a/browser/src/parser/pathGenerators/generateAFArc.ts b/browser/src/parser/pathGenerators/generateAFArc.ts index f9c6fc0..85c2211 100644 --- a/browser/src/parser/pathGenerators/generateAFArc.ts +++ b/browser/src/parser/pathGenerators/generateAFArc.ts @@ -30,11 +30,11 @@ export const generateAFArc = ( while (crsFromOrigin !== crsIntoEndpoint) { if (turnDir === 'R') { const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees(); - crsFromOrigin += delta < 1 ? delta : 1; + crsFromOrigin += delta < 0.1 ? delta : 0.1; crsFromOrigin = crsFromOrigin.normaliseDegrees(); } else { const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees(); - crsFromOrigin -= delta < 1 ? delta : 1; + crsFromOrigin -= delta < 0.1 ? delta : 0.1; crsFromOrigin = crsFromOrigin.normaliseDegrees(); } if (crsFromOrigin === crsIntoEndpoint) break; diff --git a/browser/src/parser/pathGenerators/generatePerformanceArc.ts b/browser/src/parser/pathGenerators/generatePerformanceArc.ts index 83fd31f..523fc29 100644 --- a/browser/src/parser/pathGenerators/generatePerformanceArc.ts +++ b/browser/src/parser/pathGenerators/generatePerformanceArc.ts @@ -38,12 +38,12 @@ export const generatePerformanceArc = ( let time = 0; if (turnDir === 'R') { const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; crsFromOrigin = (crsFromOrigin + increment).normaliseDegrees(); time = increment / turnRate; } else { const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; crsFromOrigin = (crsFromOrigin - increment).normaliseDegrees(); time = increment / turnRate; } @@ -70,12 +70,12 @@ export const generatePerformanceArc = ( let time = 0; if (turnDir === 'R') { const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; crsFromOrigin = (crsFromOrigin + increment).normaliseDegrees(); time = increment / turnRate; } else { const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; crsFromOrigin = (crsFromOrigin - increment).normaliseDegrees(); time = increment / turnRate; } diff --git a/browser/src/parser/pathGenerators/generateRFArc.ts b/browser/src/parser/pathGenerators/generateRFArc.ts index 4fbcc19..a2a9956 100644 --- a/browser/src/parser/pathGenerators/generateRFArc.ts +++ b/browser/src/parser/pathGenerators/generateRFArc.ts @@ -1,5 +1,5 @@ import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; -import getDistance from 'geolib/es/getDistance'; +import getPreciseDistance from 'geolib/es/getPreciseDistance'; /** * @param crsIntoEndpoint Course into arc endpoint @@ -36,7 +36,7 @@ export const generateRFArc = ( crsOrthogonalOnEndpoint = (crsIntoEndpoint - 90).normaliseDegrees(); } - const arcRad = getDistance(center, start); + const arcRad = getPreciseDistance(center, start); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse(); crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse(); @@ -50,11 +50,11 @@ export const generateRFArc = ( while (!crsOrthogonalOnOrigin.equal(crsOrthogonalOnEndpoint)) { if (turnDir === 'R') { const delta = (crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin).normaliseDegrees(); - crsOrthogonalOnOrigin += delta < 1 ? delta : 1; + crsOrthogonalOnOrigin += delta < 0.1 ? delta : 0.1; crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); } else { const delta = (crsOrthogonalOnOrigin - crsOrthogonalOnEndpoint).normaliseDegrees(); - crsOrthogonalOnOrigin -= delta < 1 ? delta : 1; + crsOrthogonalOnOrigin -= delta < 0.1 ? delta : 0.1; crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); } diff --git a/browser/src/parser/pathGenerators/generateTangentArc.ts b/browser/src/parser/pathGenerators/generateTangentArc.ts index 2a8d8b7..3508dae 100644 --- a/browser/src/parser/pathGenerators/generateTangentArc.ts +++ b/browser/src/parser/pathGenerators/generateTangentArc.ts @@ -1,5 +1,5 @@ import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; -import getDistance from 'geolib/es/getDistance'; +import getPreciseDistance from 'geolib/es/getPreciseDistance'; import { computeIntersection } from '../utils/computeIntersection'; /** @@ -56,14 +56,18 @@ export const generateTangentArc = ( } // Generate arc - const arcCenter = computeIntersection( + let arcCenter = computeIntersection( start, crsOrthogonalOnOrigin, intcArcOnCrsIntoEndpoint, crsOrthogonalOnEndpoint ); - if (!arcCenter) return null; - const arcRad = getDistance(arcCenter, start); + let arcRad = 0; + if (arcCenter) arcRad = getPreciseDistance(arcCenter, start); + else { + arcRad = getPreciseDistance(start, end) / 2; + arcCenter = computeDestinationPoint(start, arcRad, crsOrthogonalOnOrigin); + } crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse(); crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse(); @@ -74,19 +78,24 @@ export const generateTangentArc = ( crsOrthogonalOnOrigin -= crsOrthogonalOnOrigin < 1 ? crsOrthogonalOnOrigin : 1; } + let lastDistance = getPreciseDistance(start, end); while (!crsOrthogonalOnOrigin.equal(crsOrthogonalOnEndpoint)) { if (turnDir === 'R') { const delta = (crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin).normaliseDegrees(); - crsOrthogonalOnOrigin += delta < 1 ? delta : 1; + crsOrthogonalOnOrigin += delta < 0.1 ? delta : 0.1; crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); } else { const delta = (crsOrthogonalOnOrigin - crsOrthogonalOnEndpoint).normaliseDegrees(); - crsOrthogonalOnOrigin -= delta < 1 ? delta : 1; + crsOrthogonalOnOrigin -= delta < 0.1 ? delta : 0.1; crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); } const arcFix = computeDestinationPoint(arcCenter, arcRad, crsOrthogonalOnOrigin); + const newDistance = getPreciseDistance(arcFix, end); + if (lastDistance <= newDistance && lastDistance < 25) break; + lastDistance = newDistance; + line.push([arcFix.longitude, arcFix.latitude]); } } diff --git a/browser/src/parser/terminators/CD.ts b/browser/src/parser/terminators/CD.ts index e1c1193..5056874 100644 --- a/browser/src/parser/terminators/CD.ts +++ b/browser/src/parser/terminators/CD.ts @@ -1,6 +1,6 @@ import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; -import getDistance from 'geolib/es/getDistance'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; +import getPreciseDistance from 'geolib/es/getPreciseDistance'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { computeSpeed } from '../utils/computeSpeed'; @@ -23,7 +23,7 @@ export const TerminatorsCD = ( // Compute distance to fly from arc end const crsToNavaid = getGreatCircleBearing(arcEnd, navaid); - const distToNavaid = getDistance(arcEnd, navaid); + const distToNavaid = getPreciseDistance(arcEnd, navaid); let remainingDistance = leg.Distance.toMetre(); // Navaid behind us if (Math.abs(crsToNavaid - lastCourse) > 90) { diff --git a/browser/src/parser/terminators/CF.ts b/browser/src/parser/terminators/CF.ts index d5c3e4c..b4900cb 100644 --- a/browser/src/parser/terminators/CF.ts +++ b/browser/src/parser/terminators/CF.ts @@ -1,6 +1,8 @@ import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; +import getPreciseDistance from 'geolib/es/getPreciseDistance'; import Parser from '../parser'; +import { generateTangentArc } from '../pathGenerators/generateTangentArc'; import { computeIntersection } from '../utils/computeIntersection'; import { computeSpeed } from '../utils/computeSpeed'; import { computeTurnRate } from '../utils/computeTurnRate'; @@ -13,7 +15,7 @@ export const TerminatorsCF = ( ): [NavFix?, LineSegment[]?] => { const speed = computeSpeed(leg, previousFix); const crsIntoEndpoint = leg.Course.toTrue(previousFix); - const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; + const line: LineSegment[] = []; const targetFix: NavFix = { latitude: leg.WptLat, @@ -28,7 +30,15 @@ export const TerminatorsCF = ( const crsToIntercept = leg.Course.toTrue(targetFix); // Compute overfly arc - if (previousFix.isFlyOver && !lastCourse.equal(crsIntoEndpoint)) { + let arc1: LineSegment[] | null = null; + let arc2: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; + if (previousFix.isFlyOver) { + arc1 = generateTangentArc(crsIntoEndpoint, lastCourse, previousFix, targetFix, leg.TurnDir); + } else { + arc1 = [[previousFix.longitude, previousFix.latitude]]; + } + + if (previousFix.isFlyOver && (!lastCourse.equal(crsIntoEndpoint) || !lastCourse.equal(crsToIntercept))) { const turnRate = computeTurnRate(speed, Parser.AC_BANK); let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix); @@ -44,26 +54,26 @@ export const TerminatorsCF = ( let time = 0; if (leg.TurnDir === 'R') { //const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); - const increment = 1; //delta < 1 ? delta : 1; + const increment = 0.1; //delta < 1 ? delta : 1; lastCourse = (lastCourse + increment).normaliseDegrees(); time = increment / turnRate; } else { //const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); - const increment = 1; //delta < 1 ? delta : 1; + const increment = 0.1; //delta < 1 ? delta : 1; lastCourse = (lastCourse - increment).normaliseDegrees(); time = increment / turnRate; } const arcFix = computeDestinationPoint( { - latitude: line.at(-1)![1], - longitude: line.at(-1)![0], + latitude: arc2.at(-1)![1], + longitude: arc2.at(-1)![0], }, ((speed / 3600) * time).toMetre(), lastCourse ); - line.push([arcFix.longitude, arcFix.latitude]); + arc2.push([arcFix.longitude, arcFix.latitude]); // Update previousFix previousFix.latitude = arcFix.latitude; @@ -71,22 +81,54 @@ export const TerminatorsCF = ( updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix); let interceptAngle = 0; - if (leg.TurnDir === 'R') Math.abs((interceptAngle = lastCourse - crsToIntercept)); - else interceptAngle = Math.abs(crsToIntercept - lastCourse); + if (leg.TurnDir === 'R') interceptAngle = lastCourse - crsToIntercept; + else interceptAngle = crsToIntercept - lastCourse; - if (interceptAngle >= 45) break; + if (interceptAngle > 0 && interceptAngle <= 45) { + const interceptFix: NavFix = { + ...computeIntersection( + previousFix, + leg.Course.toTrue(previousFix), + targetFix, + crsToIntercept.reciprocalCourse() + )!, + isFlyOver: leg.IsFlyOver, + altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, + speed: speed, + speedConstraint: leg.SpeedLimit, + altitudeConstraint: leg.Alt, + }; + if (interceptFix.latitude) arc2.push([interceptFix.longitude, interceptFix.latitude]); + + break; + } } } - const interceptFix: NavFix = { - ...computeIntersection(previousFix, leg.Course.toTrue(previousFix), targetFix, crsToIntercept.reciprocalCourse())!, - isFlyOver: leg.IsFlyOver, - altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: speed, - speedConstraint: leg.SpeedLimit, - altitudeConstraint: leg.Alt, - }; - if (interceptFix.latitude) line.push([interceptFix.longitude, interceptFix.latitude]); + // Decide on arc + let arc; + if (arc1 && arc1.length > 1) { + const endCrs = getGreatCircleBearing( + { + latitude: arc1.at(-1)![1], + longitude: arc1.at(-1)![0], + }, + targetFix + ); + const endDist = getPreciseDistance( + { + latitude: arc1.at(-1)![1], + longitude: arc1.at(-1)![0], + }, + targetFix + ); + + if (endDist <= 25 || (endCrs <= crsIntoEndpoint + 1 && endCrs >= crsIntoEndpoint - 1)) arc = arc1; + else arc = arc2; + } else { + arc = arc2; + } + line.push(...arc); line.push([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/CI.ts b/browser/src/parser/terminators/CI.ts index 9ebaafb..a1bc65c 100644 --- a/browser/src/parser/terminators/CI.ts +++ b/browser/src/parser/terminators/CI.ts @@ -34,12 +34,12 @@ export const TerminatorsCI = ( let time = 0; if (leg.TurnDir === 'R') { const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; lastCourse = (lastCourse + increment).normaliseDegrees(); time = increment / turnRate; } else { const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; lastCourse = (lastCourse - increment).normaliseDegrees(); time = increment / turnRate; } diff --git a/browser/src/parser/terminators/DF.ts b/browser/src/parser/terminators/DF.ts index dd37b93..d2f6acc 100644 --- a/browser/src/parser/terminators/DF.ts +++ b/browser/src/parser/terminators/DF.ts @@ -1,6 +1,9 @@ +import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; -import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; +import getPreciseDistance from 'geolib/es/getPreciseDistance'; +import Parser from '../parser'; import { computeSpeed } from '../utils/computeSpeed'; +import { computeTurnRate } from '../utils/computeTurnRate'; export const TerminatorsDF = ( leg: DFTerminalEntry, @@ -9,6 +12,9 @@ export const TerminatorsDF = ( waypoint?: Waypoint ): [NavFix?, LineSegment[]?] => { const speed = computeSpeed(leg, previousFix); + const turnRate = computeTurnRate(speed, Parser.AC_BANK); + const originalCrsFromOrigin = lastCourse; + const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; const targetFix: NavFix = { latitude: leg.WptLat, @@ -21,18 +27,91 @@ export const TerminatorsDF = ( altitudeConstraint: leg.Alt, }; - const crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix); + let crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix); + let force360 = getPreciseDistance(previousFix, targetFix) < 25; - // Compute overfly - const [line, _, _lastCourse] = generateOverflyArc( - crsIntoEndpoint, - lastCourse, - previousFix, - speed, - leg.TurnDir, - previousFix.latitude.equal(targetFix.latitude) && previousFix.longitude.equal(targetFix.longitude) - ); - lastCourse = _lastCourse; + // Check if there even is an arc + if (force360 || !lastCourse.equal(crsIntoEndpoint)) { + // Turn Dir + if (!leg.TurnDir || leg.TurnDir === 'E') { + let prov = lastCourse - crsIntoEndpoint; + prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov; + leg.TurnDir = prov > 0 ? 'L' : 'R'; + } + + // Generate arc + while (!lastCourse.equal(crsIntoEndpoint)) { + let time = 0; + if (leg.TurnDir === 'R') { + const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + lastCourse = (lastCourse + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + lastCourse = (lastCourse - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + lastCourse + ); + + crsIntoEndpoint = getGreatCircleBearing(arcFix, targetFix); + + line.push([arcFix.longitude, arcFix.latitude]); + + // made a loop + if (line.length >= 3600) { + if (!force360) { + line.splice(1); + } + force360 = false; + break; + } + } + + // Second half + if (force360) { + const temp = crsIntoEndpoint; + crsIntoEndpoint = originalCrsFromOrigin; + lastCourse = temp; + + while (!lastCourse.equal(crsIntoEndpoint)) { + let time = 0; + if (leg.TurnDir === 'R') { + const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + lastCourse = (lastCourse + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); + const increment = delta < 0.1 ? delta : 0.1; + lastCourse = (lastCourse - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + lastCourse + ); + + crsIntoEndpoint = getGreatCircleBearing(arcFix, targetFix); + + line.push([arcFix.longitude, arcFix.latitude]); + } + } + } line.push([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/FC.ts b/browser/src/parser/terminators/FC.ts index a90f3a7..d499f32 100644 --- a/browser/src/parser/terminators/FC.ts +++ b/browser/src/parser/terminators/FC.ts @@ -1,8 +1,8 @@ import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; +import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import Parser from '../parser'; import { computeSpeed } from '../utils/computeSpeed'; import { computeTurnRate } from '../utils/computeTurnRate'; -import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; // NOTE: Distance not adjusted for altitude in this demo export const TerminatorsFC = ( @@ -38,12 +38,12 @@ export const TerminatorsFC = ( let time = 0; if (leg.TurnDir === 'R') { const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; lastCourse = (lastCourse + increment).normaliseDegrees(); time = increment / turnRate; } else { const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; lastCourse = (lastCourse - increment).normaliseDegrees(); time = increment / turnRate; } diff --git a/browser/src/parser/terminators/FD.ts b/browser/src/parser/terminators/FD.ts index 27c9693..c9cfe81 100644 --- a/browser/src/parser/terminators/FD.ts +++ b/browser/src/parser/terminators/FD.ts @@ -1,6 +1,6 @@ import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; -import getDistance from 'geolib/es/getDistance'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; +import getPreciseDistance from 'geolib/es/getPreciseDistance'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { computeSpeed } from '../utils/computeSpeed'; @@ -27,7 +27,7 @@ export const TerminatorsFD = ( // Compute distance to fly from arc end const crsToNavaid = getGreatCircleBearing(arcEnd, navaid); - const distToNavaid = getDistance(arcEnd, navaid); + const distToNavaid = getPreciseDistance(arcEnd, navaid); let remainingDistance = leg.Distance.toMetre(); // Navaid behind us if (Math.abs(crsToNavaid - lastCourse) > 90) { diff --git a/browser/src/parser/terminators/RF.ts b/browser/src/parser/terminators/RF.ts index 3b57d09..8fa04c9 100644 --- a/browser/src/parser/terminators/RF.ts +++ b/browser/src/parser/terminators/RF.ts @@ -1,6 +1,7 @@ import { generateRFArc } from '../pathGenerators/generateRFArc'; import { computeSpeed } from '../utils/computeSpeed'; +// NOTE: Direct entry into an RF does not calculate a usable line, given inbound course is unknown. export const TerminatorsRF = ( leg: RFTerminalEntry, previousFix: NavFix, diff --git a/browser/src/parser/terminators/TF.ts b/browser/src/parser/terminators/TF.ts index e750f80..4f169a4 100644 --- a/browser/src/parser/terminators/TF.ts +++ b/browser/src/parser/terminators/TF.ts @@ -1,7 +1,11 @@ -import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; -import Parser from '../parser'; -import { computeSpeed } from '../utils/computeSpeed'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; +import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; +import getPreciseDistance from 'geolib/es/getPreciseDistance'; +import Parser from '../parser'; +import { generateTangentArc } from '../pathGenerators/generateTangentArc'; +import { computeIntersection } from '../utils/computeIntersection'; +import { computeSpeed } from '../utils/computeSpeed'; +import { computeTurnRate } from '../utils/computeTurnRate'; export const TerminatorsTF = ( leg: TFTerminalEntry, @@ -9,68 +13,117 @@ export const TerminatorsTF = ( lastCourse: number, waypoint?: Waypoint ): [NavFix?, LineSegment[]?] => { + const speed = computeSpeed(leg, previousFix); + const line: LineSegment[] = []; + const targetFix: NavFix = { latitude: leg.WptLat, longitude: leg.WptLon, name: waypoint?.Ident ?? undefined, isFlyOver: leg.IsFlyOver, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; + const crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix); - const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; - - const trackIntoEndpoint = getGreatCircleBearing(previousFix, targetFix); - + // Compute overfly arc + let arc1: LineSegment[] | null = null; + let arc2: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; if (previousFix.isFlyOver) { - let crsIntoEndpoint = trackIntoEndpoint; + arc1 = generateTangentArc(crsIntoEndpoint, lastCourse, previousFix, targetFix, leg.TurnDir); + } else { + arc1 = [[previousFix.longitude, previousFix.latitude]]; + } - // Check if there even is an arc - if (crsIntoEndpoint !== lastCourse) { - // Turn Dir - if (!leg.TurnDir || leg.TurnDir === 'E') { - let prov = lastCourse - crsIntoEndpoint; - prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov; - leg.TurnDir = prov > 0 ? 'L' : 'R'; + if (previousFix.isFlyOver && (!lastCourse.equal(crsIntoEndpoint) || !lastCourse.equal(crsIntoEndpoint))) { + const turnRate = computeTurnRate(speed, Parser.AC_BANK); + let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix); + + // Turn Dir + if (!leg.TurnDir || leg.TurnDir === 'E') { + let prov = lastCourse - crsIntoEndpoint; + prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov; + leg.TurnDir = prov > 0 ? 'L' : 'R'; + } + + // Generate arc + while (!updatedCrsToIntercept.equal(crsIntoEndpoint)) { + let time = 0; + if (leg.TurnDir === 'R') { + //const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); + const increment = 0.1; //delta < 1 ? delta : 1; + lastCourse = (lastCourse + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + //const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); + const increment = 0.1; //delta < 1 ? delta : 1; + lastCourse = (lastCourse - increment).normaliseDegrees(); + time = increment / turnRate; } - // Generate arc - let condition = false; - do { - if (leg.TurnDir === 'R') { - const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); - lastCourse += delta < 1 ? delta : 1; - lastCourse = lastCourse.normaliseDegrees(); - } else { - const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); - lastCourse -= delta < 1 ? delta : 1; - lastCourse = lastCourse.normaliseDegrees(); - } + const arcFix = computeDestinationPoint( + { + latitude: arc2.at(-1)![1], + longitude: arc2.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + lastCourse + ); - const arcFix = computeDestinationPoint( - { - latitude: line.at(-1)![1], - longitude: line.at(-1)![0], - }, - ((previousFix.speed ? previousFix.speed : Parser.AC_SPEED) / 3600).toMetre(), - lastCourse - ); + arc2.push([arcFix.longitude, arcFix.latitude]); - line.push([arcFix.longitude, arcFix.latitude]); + // Update previousFix + previousFix.latitude = arcFix.latitude; + previousFix.longitude = arcFix.longitude; + updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix); - crsIntoEndpoint = getGreatCircleBearing(arcFix, targetFix); + let interceptAngle = 0; + if (leg.TurnDir === 'R') interceptAngle = lastCourse - crsIntoEndpoint; + else interceptAngle = crsIntoEndpoint - lastCourse; - if (leg.TurnDir === 'R') { - condition = crsIntoEndpoint > trackIntoEndpoint; - } else { - condition = crsIntoEndpoint < trackIntoEndpoint; - } - } while (condition); + if (interceptAngle > 0 && interceptAngle <= 45) { + const interceptFix: NavFix = { + ...computeIntersection(previousFix, crsIntoEndpoint, targetFix, crsIntoEndpoint.reciprocalCourse())!, + isFlyOver: leg.IsFlyOver, + altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, + speed: speed, + speedConstraint: leg.SpeedLimit, + altitudeConstraint: leg.Alt, + }; + if (interceptFix.latitude) line.push([interceptFix.longitude, interceptFix.latitude]); + + break; + } } } + // Decide on arc + let arc; + if (arc1 && arc1.length > 1) { + const endCrs = getGreatCircleBearing( + { + latitude: arc1.at(-1)![1], + longitude: arc1.at(-1)![0], + }, + targetFix + ); + const endDist = getPreciseDistance( + { + latitude: arc1.at(-1)![1], + longitude: arc1.at(-1)![0], + }, + targetFix + ); + + if (endDist <= 25 || (endCrs <= crsIntoEndpoint + 1 && endCrs >= crsIntoEndpoint - 1)) arc = arc1; + else arc = arc2; + } else { + arc = arc2; + } + line.push(...arc); + line.push([targetFix.longitude, targetFix.latitude]); return [targetFix, line]; diff --git a/browser/src/parser/terminators/VD.ts b/browser/src/parser/terminators/VD.ts index c5be10f..3c45020 100644 --- a/browser/src/parser/terminators/VD.ts +++ b/browser/src/parser/terminators/VD.ts @@ -1,6 +1,6 @@ import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; -import getDistance from 'geolib/es/getDistance'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; +import getPreciseDistance from 'geolib/es/getPreciseDistance'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { computeSpeed } from '../utils/computeSpeed'; @@ -24,7 +24,7 @@ export const TerminatorsVD = ( // Compute distance to fly from arc end const crsToNavaid = getGreatCircleBearing(arcEnd, navaid); - const distToNavaid = getDistance(arcEnd, navaid); + const distToNavaid = getPreciseDistance(arcEnd, navaid); let remainingDistance = leg.Distance.toMetre(); // Navaid behind us if (Math.abs(crsToNavaid - lastCourse) > 90) { diff --git a/browser/src/parser/terminators/VI.ts b/browser/src/parser/terminators/VI.ts index 7645452..c2c4c8b 100644 --- a/browser/src/parser/terminators/VI.ts +++ b/browser/src/parser/terminators/VI.ts @@ -1,10 +1,10 @@ +import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import Parser from '../parser'; import { computeIntersection } from '../utils/computeIntersection'; import { computeSpeed } from '../utils/computeSpeed'; import { computeTurnRate } from '../utils/computeTurnRate'; import { getCourseAndFixForIntercepts } from '../utils/getCourseAndFixForIntercepts'; -import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; // NOTE: No wind adjustments to be made, no clue how *that* would draw export const TerminatorsVI = ( @@ -35,12 +35,12 @@ export const TerminatorsVI = ( let time = 0; if (leg.TurnDir === 'R') { const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; lastCourse = (lastCourse + increment).normaliseDegrees(); time = increment / turnRate; } else { const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); - const increment = delta < 1 ? delta : 1; + const increment = delta < 0.1 ? delta : 0.1; lastCourse = (lastCourse - increment).normaliseDegrees(); time = increment / turnRate; } diff --git a/browser/src/parser/utils/computeIntersection.ts b/browser/src/parser/utils/computeIntersection.ts index 5589622..e4aaaf0 100644 --- a/browser/src/parser/utils/computeIntersection.ts +++ b/browser/src/parser/utils/computeIntersection.ts @@ -5,7 +5,12 @@ * @param brng2 bearing from Point 2 * @returns Intersection point */ -export const computeIntersection = (p1: NavFix, brng1: number, p2: NavFix, brng2: number): NavFix | undefined => { +export const computeIntersection = ( + p1: NavFix, + brng1: number, + p2: NavFix, + brng2: number +): NavFix | undefined | null => { if (isNaN(brng1)) throw new TypeError(`invalid brng1 ${brng1}`); if (isNaN(brng2)) throw new TypeError(`invalid brng2 ${brng2}`); @@ -43,7 +48,7 @@ export const computeIntersection = (p1: NavFix, brng1: number, p2: NavFix, brng2 const α2 = θ21 - θ23; // angle 1-2-3 if (Math.sin(α1) == 0 && Math.sin(α2) == 0) return undefined; // infinite intersections - if (Math.sin(α1) * Math.sin(α2) < 0) return undefined; // ambiguous intersection (antipodal/360°) + if (Math.sin(α1) * Math.sin(α2) < 0) return p2; // ambiguous intersection (antipodal/360°) const cosα3 = -Math.cos(α1) * Math.cos(α2) + Math.sin(α1) * Math.sin(α2) * Math.cos(δ12); diff --git a/browser/src/types/navdata.d.ts b/browser/src/types/navdata.d.ts index a776733..e650321 100644 --- a/browser/src/types/navdata.d.ts +++ b/browser/src/types/navdata.d.ts @@ -75,6 +75,7 @@ export declare global { ICAO: string; FullName: string; RwyID?: number; + Proc: 1 | 2 | 3; }; type NavFix = {