From 2c99b701ce2ddbd46b1edf2225e558e476c3014b Mon Sep 17 00:00:00 2001 From: Kilian Hofmann Date: Sun, 13 Jul 2025 22:51:43 +0200 Subject: [PATCH] Types AF;RF --- .vscode/settings.json | 3 + README.md | 4 +- .../public/navdata/Runways.json | 0 .../public/navdata/TermID_10394.json | 0 .../public/navdata/TermID_10395.json | 0 .../public/navdata/TermID_10475.json | 0 .../public/navdata/TermID_10480.json | 0 .../public/navdata/TermID_10482.json | 0 .../public/navdata/TermID_10485.json | 0 .../public/navdata/TermID_10653.json | 0 .../public/navdata/TermID_10654.json | 0 .../public/navdata/TermID_10657.json | 0 .../public/navdata/TermID_10659.json | 0 .../public/navdata/TermID_10679.json | 0 .../public/navdata/TermID_11798.json | 0 .../public/navdata/TermID_11909.json | 0 .../public/navdata/TermID_12765.json | 0 .../public/navdata/TermID_67790.json | 0 .../public/navdata/TermID_67794.json | 0 .../public/navdata/Terminals.json | 0 .../public/navdata/Waypoints.json | 0 geojson.js | 324 +++++++++++++++--- 22 files changed, 286 insertions(+), 45 deletions(-) create mode 100644 .vscode/settings.json rename Runways.json => browser/public/navdata/Runways.json (100%) rename TermID_10394.json => browser/public/navdata/TermID_10394.json (100%) rename TermID_10395.json => browser/public/navdata/TermID_10395.json (100%) rename TermID_10475.json => browser/public/navdata/TermID_10475.json (100%) rename TermID_10480.json => browser/public/navdata/TermID_10480.json (100%) rename TermID_10482.json => browser/public/navdata/TermID_10482.json (100%) rename TermID_10485.json => browser/public/navdata/TermID_10485.json (100%) rename TermID_10653.json => browser/public/navdata/TermID_10653.json (100%) rename TermID_10654.json => browser/public/navdata/TermID_10654.json (100%) rename TermID_10657.json => browser/public/navdata/TermID_10657.json (100%) rename TermID_10659.json => browser/public/navdata/TermID_10659.json (100%) rename TermID_10679.json => browser/public/navdata/TermID_10679.json (100%) rename TermID_11798.json => browser/public/navdata/TermID_11798.json (100%) rename TermID_11909.json => browser/public/navdata/TermID_11909.json (100%) rename TermID_12765.json => browser/public/navdata/TermID_12765.json (100%) rename TermID_67790.json => browser/public/navdata/TermID_67790.json (100%) rename TermID_67794.json => browser/public/navdata/TermID_67794.json (100%) rename Terminals.json => browser/public/navdata/Terminals.json (100%) rename Waypoints.json => browser/public/navdata/Waypoints.json (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bded346 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["intc"] +} diff --git a/README.md b/README.md index 2850a41..0f8e607 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ LGAV BIBE1K SID (Cycle 2507, ID 10653) (`NavID`, `NavLat`, `NavLon`, `NavDist`, `NavBear`). - Arc radius shall be `NavDist`. - Arc center shall be navaid identified by `NavID`, `NavLat`, `NavLon`. -- `Course` shall be the boundary radial on which the arc begins (preceding fix lies hereupon). +- `Course` shall be the boundary radial on which the arc begins, measured from the navaid + identified by (`NavID`, `NavLat`, `NavLon`, `NavDist`, `NavBear`). - Arc and turn shall be flown in direction specified by `TurnDir`. ### Units @@ -512,7 +513,6 @@ LFRN GODA5R SID (cycle 2507, ID 10485) ### Instructions - From preceding fix, fly a smooth arc to the fix identified by (`WptID`, `WptLat`, `WptLon`). -- Arc radius shall be `NavDist`. - Arc center shall be navaid identified by `CenterID`, `CenterLat`, `CenterLon`. - Arc and turn shall be flown in direction specified by `TurnDir`. - `Distance` shall be the track miles along the curved path diff --git a/Runways.json b/browser/public/navdata/Runways.json similarity index 100% rename from Runways.json rename to browser/public/navdata/Runways.json diff --git a/TermID_10394.json b/browser/public/navdata/TermID_10394.json similarity index 100% rename from TermID_10394.json rename to browser/public/navdata/TermID_10394.json diff --git a/TermID_10395.json b/browser/public/navdata/TermID_10395.json similarity index 100% rename from TermID_10395.json rename to browser/public/navdata/TermID_10395.json diff --git a/TermID_10475.json b/browser/public/navdata/TermID_10475.json similarity index 100% rename from TermID_10475.json rename to browser/public/navdata/TermID_10475.json diff --git a/TermID_10480.json b/browser/public/navdata/TermID_10480.json similarity index 100% rename from TermID_10480.json rename to browser/public/navdata/TermID_10480.json diff --git a/TermID_10482.json b/browser/public/navdata/TermID_10482.json similarity index 100% rename from TermID_10482.json rename to browser/public/navdata/TermID_10482.json diff --git a/TermID_10485.json b/browser/public/navdata/TermID_10485.json similarity index 100% rename from TermID_10485.json rename to browser/public/navdata/TermID_10485.json diff --git a/TermID_10653.json b/browser/public/navdata/TermID_10653.json similarity index 100% rename from TermID_10653.json rename to browser/public/navdata/TermID_10653.json diff --git a/TermID_10654.json b/browser/public/navdata/TermID_10654.json similarity index 100% rename from TermID_10654.json rename to browser/public/navdata/TermID_10654.json diff --git a/TermID_10657.json b/browser/public/navdata/TermID_10657.json similarity index 100% rename from TermID_10657.json rename to browser/public/navdata/TermID_10657.json diff --git a/TermID_10659.json b/browser/public/navdata/TermID_10659.json similarity index 100% rename from TermID_10659.json rename to browser/public/navdata/TermID_10659.json diff --git a/TermID_10679.json b/browser/public/navdata/TermID_10679.json similarity index 100% rename from TermID_10679.json rename to browser/public/navdata/TermID_10679.json diff --git a/TermID_11798.json b/browser/public/navdata/TermID_11798.json similarity index 100% rename from TermID_11798.json rename to browser/public/navdata/TermID_11798.json diff --git a/TermID_11909.json b/browser/public/navdata/TermID_11909.json similarity index 100% rename from TermID_11909.json rename to browser/public/navdata/TermID_11909.json diff --git a/TermID_12765.json b/browser/public/navdata/TermID_12765.json similarity index 100% rename from TermID_12765.json rename to browser/public/navdata/TermID_12765.json diff --git a/TermID_67790.json b/browser/public/navdata/TermID_67790.json similarity index 100% rename from TermID_67790.json rename to browser/public/navdata/TermID_67790.json diff --git a/TermID_67794.json b/browser/public/navdata/TermID_67794.json similarity index 100% rename from TermID_67794.json rename to browser/public/navdata/TermID_67794.json diff --git a/Terminals.json b/browser/public/navdata/Terminals.json similarity index 100% rename from Terminals.json rename to browser/public/navdata/Terminals.json diff --git a/Waypoints.json b/browser/public/navdata/Waypoints.json similarity index 100% rename from Waypoints.json rename to browser/public/navdata/Waypoints.json diff --git a/geojson.js b/geojson.js index d2a765f..f855bde 100644 --- a/geojson.js +++ b/geojson.js @@ -1,6 +1,7 @@ import fs from "fs"; import geo from "geojson"; import geolib from "geolib"; +import magvar from "magvar"; /* Constants */ const π = Math.PI; @@ -12,11 +13,13 @@ const AC_VS = 3000; //const PROCEDURE_ID = 10394; //const PROCEDURE_ID = 10395; /* LFRK */ -const MAGVAR = 0; +//const MAGVAR = 0; //const PROCEDURE_ID = 10475; //const PROCEDURE_ID = 10480; //const PROCEDURE_ID = 10482; -const PROCEDURE_ID = 10485; +//const PROCEDURE_ID = 10485; +const MAGVAR = -5; +const PROCEDURE_ID = 10653; /* Prototypes */ Number.prototype.toRadians = function () { @@ -33,11 +36,18 @@ Number.prototype.flipCourse = function () { Number.prototype.normaliseDegrees = function () { return this >= 360 ? this - 360 : this < 0 ? this + 360 : this; }; -Number.prototype.toTrue = function () { +Number.prototype.toTrue = function (fix) { + // IRL Magvar + //return (this - magvar.magvar(fix.lat, fix.lon)).normaliseDegrees(); + // NG Magvar return (this - MAGVAR).normaliseDegrees(); }; /* Functions */ +/** + * + * @param {[number, number]} line line segment + */ const updateLastCourse = (line) => { lastCourse = geolib.getGreatCircleBearing( { @@ -50,7 +60,20 @@ const updateLastCourse = (line) => { } ); }; +/** + * + * @param {number} nmi Nautical Miles + * @returns Metres + */ const nmiToMetre = (nmi) => nmi * 1852.0; +/** + * + * @param {{lat: number, lon: number}} p1 Point 1 + * @param {number} brng1 Bearing from Point 1 + * @param {{lat: number, lon: number}} p2 Point 2 + * @param {number} brng2 bearing from Point 2 + * @returns {{lat: number, lon: number}} Intersection point + */ const computeIntersection = (p1, brng1, p2, brng2) => { if (isNaN(brng1)) throw new TypeError(`invalid brng1 ${brng1}`); if (isNaN(brng2)) throw new TypeError(`invalid brng2 ${brng2}`); @@ -126,6 +149,15 @@ const computeIntersection = (p1, brng1, p2, brng2) => { return { lat, lon, name: "INTC", alt: p1.alt }; }; +/** + * + * @param {number} inbdCrs Course into arc endpoint + * @param {*} outbCrs Course into arc origin point + * @param {{lat: number, lon: number}} arcStart Arc origin point + * @param {{lat: number, lon: number}} legEnd Arc endpoint + * @param {"R"|"L"|"E"|null} turnDir Turn direction + * @returns {[[number, number]]} Line segments + */ const generateTangentialArc = (inbdCrs, outbCrs, arcStart, legEnd, turnDir) => { const line = []; @@ -219,6 +251,14 @@ const generateTangentialArc = (inbdCrs, outbCrs, arcStart, legEnd, turnDir) => { return line; }; +/** + * + * @param {number} inbdCrs Course into arc endpoint + * @param {number} outbCrs Course into arc origin point + * @param {{lat: number, lon: number}} arcStart Arc origin point + * @param {"R"|"L"|"E"|null} turnDir Turn direction + * @returns {[[number, number]]} Line segments + */ const generatePerformanceArc = (inbdCrs, outbCrs, arcStart, turnDir) => { const line = [[arcStart.lon, arcStart.lat]]; @@ -260,16 +300,147 @@ const generatePerformanceArc = (inbdCrs, outbCrs, arcStart, turnDir) => { return line.slice(1); }; -const getCourseAndFixForInterceptions = (leg) => { +/** + * + * @param {number} inbdCrs Course into arc endpoint + * @param {number} outbCrs Course into arc origin point + * @param {{lat: number, lon: number}} arcStart Arc origin point + * @param {{lat: number, lon: number}} center Arc center point + * @param {"R"|"L"|"E"|null} turnDir + * @returns {[[number, number]]} Line segments + */ +const generateRFArc = (inbdCrs, outbCrs, arcStart, center, turnDir) => { + const line = []; + + if (inbdCrs !== outbCrs) { + // Turn Dir + if (!turnDir || turnDir === "E") { + let prov = outbCrs - inbdCrs; + prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov; + turnDir = prov > 0 ? "L" : "R"; + } + + let startPerpCrs; + let endPerpCrs; + if (turnDir === "R") { + startPerpCrs = (outbCrs + 90).normaliseDegrees(); + endPerpCrs = (inbdCrs + 90).normaliseDegrees(); + } else { + startPerpCrs = (outbCrs - 90).normaliseDegrees(); + endPerpCrs = (inbdCrs - 90).normaliseDegrees(); + } + + const arcRad = geolib.getDistance( + { + latitude: center.lat, + longitude: center.lon, + }, + { + latitude: arcStart.lat, + longitude: arcStart.lon, + } + ); + + startPerpCrs = startPerpCrs.flipCourse(); + endPerpCrs = endPerpCrs.flipCourse(); + // Start turn immediately + if (turnDir === "R") { + startPerpCrs += startPerpCrs < 1 ? startPerpCrs : 1; + } else { + startPerpCrs -= startPerpCrs < 1 ? startPerpCrs : 1; + } + + while (startPerpCrs !== endPerpCrs) { + if (turnDir === "R") { + const delta = (endPerpCrs - startPerpCrs).normaliseDegrees(); + startPerpCrs += delta < 1 ? delta : 1; + startPerpCrs = startPerpCrs.normaliseDegrees(); + } else { + const delta = (startPerpCrs - endPerpCrs).normaliseDegrees(); + startPerpCrs -= delta < 1 ? delta : 1; + startPerpCrs = startPerpCrs.normaliseDegrees(); + } + if (startPerpCrs === endPerpCrs) break; + + const arcFix = geolib.computeDestinationPoint( + center, + arcRad, + startPerpCrs + ); + + line.push([arcFix.longitude, arcFix.latitude]); + } + } + + return line; +}; +/** + * + * @param {number} inbdCrs Course into arc endpoint + * @param {number} outbCrs Course into arc origin point + * @param {{lat: number, lon: number}} arcStart Arc origin point + * @param {{lat: number, lon: number}} center Arc center point + * @param {number} radius Arc radius in nmi + * @param {"R"|"L"|"E"|null} turnDir + * @returns {[[number, number]]} Line segments + */ +const generateAFArc = (inbdCrs, outbCrs, arcStart, center, radius, turnDir) => { + const line = [[arcStart.lon, arcStart.lat]]; + + if (inbdCrs !== outbCrs) { + // Turn Dir + if (!turnDir || turnDir === "E") { + let prov = outbCrs - inbdCrs; + prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov; + turnDir = prov > 0 ? "L" : "R"; + } + + while (outbCrs !== inbdCrs) { + if (turnDir === "R") { + const delta = (inbdCrs - outbCrs).normaliseDegrees(); + outbCrs += delta < 1 ? delta : 1; + outbCrs = outbCrs.normaliseDegrees(); + } else { + const delta = (outbCrs - inbdCrs).normaliseDegrees(); + outbCrs -= delta < 1 ? delta : 1; + outbCrs = outbCrs.normaliseDegrees(); + } + if (outbCrs === inbdCrs) break; + + const arcFix = geolib.computeDestinationPoint( + center, + nmiToMetre(radius), + outbCrs + ); + + line.push([arcFix.longitude, arcFix.latitude]); + } + } + + return line; +}; +const getCourseAndFixForInterceptions = (leg, origin) => { switch (leg.TrackCode) { case "CF": { + const fix = { lat: leg.WptLat, lon: leg.WptLon }; + return [leg.Course.flipCourse().toTrue(fix), fix]; + } + case "FM": { + const fix = { lat: leg.WptLat, lon: leg.WptLon }; + return [leg.Course.toTrue(fix), fix]; + } + case "TF": { return [ - leg.Course.flipCourse().toTrue(), + geolib.getGreatCircleBearing( + { latitude: origin.lat, longitude: origin.lon }, + { latitude: leg.WptLat, longitude: leg.WptLon } + ), { lat: leg.WptLat, lon: leg.WptLon }, ]; } - case "FM": { - return [leg.Course.toTrue(), { lat: leg.WptLat, lon: leg.WptLon }]; + case "AF": { + const fix = { lat: leg.WptLat, lon: leg.WptLon }; + return [leg.Course.flipCourse().toTrue(fix), fix]; } } }; @@ -322,21 +493,25 @@ const parseAltitude = (alt) => { }; /* Data */ -const waypoints = JSON.parse(fs.readFileSync("Waypoints.json")); -const terminal = JSON.parse(fs.readFileSync("Terminals.json")).filter( - ({ ID }) => ID === PROCEDURE_ID -)[0]; -const runway = JSON.parse(fs.readFileSync("Runways.json")).filter( - ({ ID }) => ID === terminal.RwyID -)[0]; -const procedure = JSON.parse(fs.readFileSync(`TermID_${PROCEDURE_ID}.json`)); +const waypoints = JSON.parse( + fs.readFileSync("browser/public/navdata/Waypoints.json") +); +const terminal = JSON.parse( + fs.readFileSync("browser/public/navdata/Terminals.json") +).filter(({ ID }) => ID === PROCEDURE_ID)[0]; +const runway = JSON.parse( + fs.readFileSync("browser/public/navdata/Runways.json") +).filter(({ ID }) => ID === terminal.RwyID)[0]; +const procedure = JSON.parse( + fs.readFileSync(`browser/public/navdata/TermID_${PROCEDURE_ID}.json`) +); /* Output */ const points = [ { lat: runway.Latitude, lon: runway.Longitude, alt: runway.Elevation }, ]; const lines = []; -let lastCourse = 0; +let lastCourse = runway.TrueHeading; /* Main */ for (let index = 0; index < procedure.length; index++) { @@ -344,7 +519,41 @@ for (let index = 0; index < procedure.length; index++) { const waypoint = waypoints.filter(({ ID }) => ID === leg.WptID)[0]; switch (leg.TrackCode) { - case "AF": + case "AF": { + // Push in ending waypoint + points.push({ + lat: leg.WptLat, + lon: leg.WptLon, + name: waypoint?.Ident ?? undefined, + "marker-color": leg.IsFlyOver !== 0 ? "#ff0000" : undefined, + isFlyOver: leg.IsFlyOver !== 0, + alt: points.at(-1).alt, + }); + + const arcEndCrs = geolib.getGreatCircleBearing( + { + latitude: leg.NavLat, + longitude: leg.NavLon, + }, + { + latitude: leg.WptLat, + longitude: leg.WptLon, + } + ); + + const line = generateAFArc( + arcEndCrs, + leg.Course.toTrue({ lat: leg.NavLat, lon: leg.NavLon }), + points.at(-2), + { lat: leg.NavLat, lon: leg.NavLon }, + leg.NavDist, + leg.TurnDir + ); + lines.push({ line }); + updateLastCourse(line); + + break; + } case "CA": case "CD": break; @@ -360,8 +569,8 @@ for (let index = 0; index < procedure.length; index++) { }); const line = handleTurnAtFix( - leg.Course.toTrue(), - leg.Course.toTrue(), + leg.Course.toTrue(points.at(-1)), + leg.Course.toTrue(points.at(-1)), points.at(-2), points.at(-1), leg.TurnDir, @@ -377,20 +586,21 @@ for (let index = 0; index < procedure.length; index++) { case "CI": { // Course into the destination fix and said fix const [inbdCrs, fix] = getCourseAndFixForInterceptions( - procedure[index + 1] + procedure[index + 1], + points.at(-1) ); // Compute INTC const intc = computeIntersection( points.at(-1), - leg.Course.toTrue(), + leg.Course.toTrue(fix), fix, inbdCrs ); const line = handleTurnAtFix( inbdCrs, - leg.Course.toTrue(), + leg.Course.toTrue(fix), points.at(-1), intc, leg.TurnDir, @@ -401,7 +611,7 @@ for (let index = 0; index < procedure.length; index++) { const intc2 = computeIntersection( { lat: line.at(-1)[1], lon: line.at(-1)[0] }, - leg.Course.toTrue(), + leg.Course.toTrue(fix), fix, inbdCrs ); @@ -420,20 +630,20 @@ for (let index = 0; index < procedure.length; index++) { } case "CR": { // Course into the destination fix - const inbdCrs = leg.Course.toTrue(); + const inbdCrs = leg.Course.toTrue(points.at(-1)); // Compute INTC const intc = computeIntersection( points.at(-1), inbdCrs, { lat: leg.NavLat, lon: leg.NavLon }, - leg.NavBear.toTrue() + leg.NavBear.toTrue({ lat: leg.NavLat, lon: leg.NavLon }) ); points.push(intc); const line = handleTurnAtFix( inbdCrs, - leg.Course.toTrue(), + leg.Course.toTrue(points.at(-2)), points.at(-2), points.at(-1), leg.TurnDir, @@ -457,12 +667,12 @@ for (let index = 0; index < procedure.length; index++) { longitude: points.at(-1).lon, }, nmiToMetre(10), - leg.Course.toTrue() + leg.Course.toTrue(points.at(-1)) ); const line = handleTurnAtFix( - leg.Course.toTrue(), - leg.Course.toTrue(), + leg.Course.toTrue(points.at(-1)), + leg.Course.toTrue(points.at(-1)), points.at(-1), { lat: end.latitude, lon: end.longitude }, leg.TurnDir, @@ -479,8 +689,35 @@ for (let index = 0; index < procedure.length; index++) { case "HM": case "IF": case "PI": - case "RF": break; + case "RF": { + // Push in ending waypoint + points.push({ + lat: leg.WptLat, + lon: leg.WptLon, + name: waypoint?.Ident ?? undefined, + "marker-color": leg.IsFlyOver !== 0 ? "#ff0000" : undefined, + isFlyOver: leg.IsFlyOver !== 0, + alt: points.at(-1).alt, + }); + + const [inbdCrs] = getCourseAndFixForInterceptions( + procedure[index + 1], + points.at(-1) + ); + + const line = generateRFArc( + inbdCrs, + lastCourse, + points.at(-1), + { lat: leg.CenterLat, lon: leg.CenterLon }, + leg.TurnDir + ); + lines.push({ line }); + updateLastCourse(line); + + break; + } case "TF": { // Push in ending waypoint points.push({ @@ -523,7 +760,7 @@ for (let index = 0; index < procedure.length; index++) { ((parseAltitude(leg.Alt) - points.at(-1).alt) / AC_VS) * (AC_SPEED / 60) ), - leg.Course.toTrue() + leg.Course.toTrue(points.at(-1)) ); points.push({ lat: end.latitude, @@ -535,8 +772,8 @@ for (let index = 0; index < procedure.length; index++) { }); const line = handleTurnAtFix( - leg.Course.toTrue(), - leg.Course.toTrue(), + leg.Course.toTrue(points.at(-2)), + leg.Course.toTrue(points.at(-2)), points.at(-2), { lat: end.latitude, lon: end.longitude }, leg.TurnDir, @@ -558,7 +795,7 @@ for (let index = 0; index < procedure.length; index++) { }, //NOTE: Does not account for slant nmiToMetre(leg.Distance), - leg.Course.toTrue() + leg.Course.toTrue(points.at(-1)) ); points.push({ lat: end.latitude, @@ -570,8 +807,8 @@ for (let index = 0; index < procedure.length; index++) { }); const line = handleTurnAtFix( - leg.Course.toTrue(), - leg.Course.toTrue(), + leg.Course.toTrue(points.at(-2)), + leg.Course.toTrue(points.at(-2)), points.at(-2), { lat: end.latitude, lon: end.longitude }, leg.TurnDir, @@ -588,20 +825,21 @@ for (let index = 0; index < procedure.length; index++) { // Course into the destination fix and said fix const [inbdCrs, fix] = getCourseAndFixForInterceptions( - procedure[index + 1] + procedure[index + 1], + points.at(-1) ); // Compute INTC const intc = computeIntersection( points.at(-1), - leg.Course.toTrue(), + leg.Course.toTrue(fix), fix, inbdCrs ); const line = handleTurnAtFix( inbdCrs, - leg.Course.toTrue(), + leg.Course.toTrue(fix), points.at(-1), intc, leg.TurnDir, @@ -612,7 +850,7 @@ for (let index = 0; index < procedure.length; index++) { const intc2 = computeIntersection( { lat: line.at(-1)[1], lon: line.at(-1)[0] }, - leg.Course.toTrue(), + leg.Course.toTrue(fix), fix, inbdCrs ); @@ -631,12 +869,12 @@ for (let index = 0; index < procedure.length; index++) { longitude: points.at(-1).lon, }, nmiToMetre(10), - leg.Course.toTrue() + leg.Course.toTrue(points.at(-1)) ); const line = handleTurnAtFix( - leg.Course.toTrue(), - leg.Course.toTrue(), + leg.Course.toTrue(points.at(-1)), + leg.Course.toTrue(points.at(-1)), points.at(-1), { lat: end.latitude, lon: end.longitude }, leg.TurnDir,