Types CF/I/R;FM;TF;VA/D/I/M

This commit is contained in:
Kilian Hofmann 2025-07-13 04:52:26 +02:00
parent 858f8e8120
commit 5b39d61cdb
4 changed files with 546 additions and 146 deletions

4
.gitignore vendored
View File

@ -1 +1,3 @@
node_modules/
node_modules/
output.json

View File

@ -64,6 +64,7 @@ LGAV BIBE1L SID (Cycle 2507, ID 10654)
The leg terminates upon reaching `Alt`.
This intercept point then becomes the origin fix of the succeeding leg.
This new origin is an implicit overfly.
## Course to DME Distance (CD)
@ -92,6 +93,7 @@ LGAV BIBE2F SID (Cycle 2507, ID 10657)
The leg terminates upon reaching `Distance`.
This intercept point then becomes the origin fix of the succeeding leg.
This new origin is an implicit overfly.
## Course to Fix (CF)
@ -155,6 +157,7 @@ LGAV BIBE1L SID (Cycle 2507, ID 10654)
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 new origin can never be an overfly due to the intercept nature.
## Course to Radial Termination (CR)
@ -234,6 +237,7 @@ LGAV BIBE2F SID (Cycle 2507, ID 10657)
The leg terminates upon reaching `Alt`.
This intercept point then becomes the origin fix of the succeeding leg.
This new origin is an implicit overfly.
## Track from Fix for Distance (FC)
@ -255,12 +259,13 @@ LIED CAR6F SID (Cycle 2507, ID 11798)
### Instructions
- From the fix identified by (`WptID`, `WptLat`, `WptLon`) *or*
(`NavID`, `NavLat`, `NavLon`, `NavDist`, `NavBear`), fly for `Distance`.
(`NavID`, `NavLat`, `NavLon`, `NavDist`, `NavBear`), fly `Course` until reaching `Distance`.
### Units
- `NavBear` and `Course` are in **degrees magnetic**.
- `NavDist` and `Distance` are in **nmi**. `NavDist` and `Distance` are geodesic.
- `IsFlyOver`: Boolean indicating if fix is a mandatory fly over.
### Notes
@ -277,7 +282,6 @@ LGAV BIBE2T SID (Cycle 2507, ID 10659)
### Minimum Required Fields
- `WptID`: FixIdentifier
- `IsFlyOver`: FlyOver
- `NavID`: RecommendedNavaid
- `NavBear`: Theta
- `NavDist`: RHO
@ -294,7 +298,12 @@ LGAV BIBE2T SID (Cycle 2507, ID 10659)
- `NavBear` and `Course` are in **degrees magnetic**.
- `NavDist` and `Distance` are in **nmi**. `NavDist` is geodesic, `Distance` is slant.
- `IsFlyOver`: Boolean indicating if fix is a mandatory fly over.
### Notes
The leg terminates upon reaching `Distance`.
This intercept point then becomes the origin fix of the succeeding leg.
This new origin is an implicit overfly.
## From Fix to Manual termination (FM)
@ -522,7 +531,7 @@ Example has `NavBear` set to `null`, significance of the inbound tangential trac
Same for the `Course`, which is set, but lacks any documentation.
## Tract to Fix (TF)
## Track to Fix (TF)
### Example
@ -556,7 +565,7 @@ LFRK LGL4X SID (Cycle 2507, ID 10475)
### Instructions
- From preceding fix, fly heading specified in `Course` until reaching `Alt`
- From preceding fix, fly heading specified in `Course` until reaching `Alt`.
- **No wind corrections shall be applied**
### Units
@ -568,6 +577,7 @@ LFRK LGL4X SID (Cycle 2507, ID 10475)
The leg terminates upon reaching `Alt`.
This intercept point then becomes the origin fix of the succeeding leg.
This new origin is an implicit overfly.
## Heading to DME Distance (VD)
@ -597,6 +607,7 @@ LFRK NEVI4Y SID (Cycle 2507, ID 10482)
The leg terminates upon reaching `Distance`.
This intercept point then becomes the origin fix of the succeeding leg.
This new origin is an implicit overfly.
## Heading to Intercept (VI)
@ -623,6 +634,7 @@ LFRK LUSI4Y SID (Cycle 2507, ID 10480)
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 new origin can never be an overfly due to the intercept nature.
## Heading to Manual Termination (VM)

View File

@ -2,18 +2,56 @@ import fs from "fs";
import geo from "geojson";
import geolib from "geolib";
/* Constants */
const π = Math.PI;
const AC_SPEED = 250;
const AC_VS = 3000;
// ADJ to location, (NG), E is neg.
/* LFPV */
//const MAGVAR = -1;
//const PROCEDURE_ID = 10394;
//const PROCEDURE_ID = 10395;
/* LFRK */
const MAGVAR = 0;
//const PROCEDURE_ID = 10475;
//const PROCEDURE_ID = 10480;
//const PROCEDURE_ID = 10482;
const PROCEDURE_ID = 10485;
/* Prototypes */
Number.prototype.toRadians = function () {
return (this * Math.PI) / 180;
};
Number.prototype.toDegrees = function () {
return (this * 180) / Math.PI;
};
Number.prototype.flipCourse = function () {
let inv = this + 180;
inv = inv >= 360 ? inv - 360 : inv;
return inv;
};
Number.prototype.normaliseDegrees = function () {
return this >= 360 ? this - 360 : this < 0 ? this + 360 : this;
};
Number.prototype.toTrue = function () {
return (this - MAGVAR).normaliseDegrees();
};
/* Functions */
const updateLastCourse = (line) => {
lastCourse = geolib.getGreatCircleBearing(
{
latitude: line.at(-2)[1],
longitude: line.at(-2)[0],
},
{
latitude: line.at(-1)[1],
longitude: line.at(-1)[0],
}
);
};
const nmiToMetre = (nmi) => nmi * 1852.0;
const intersection = (p1, brng1, p2, brng2) => {
const computeIntersection = (p1, brng1, p2, brng2) => {
if (isNaN(brng1)) throw new TypeError(`invalid brng1 ${brng1}`);
if (isNaN(brng2)) throw new TypeError(`invalid brng2 ${brng2}`);
@ -86,33 +124,223 @@ const intersection = (p1, brng1, p2, brng2) => {
const lat = φ3.toDegrees();
const lon = λ3.toDegrees();
return { lat, lon, name: "INTC" };
return { lat, lon, name: "INTC", alt: p1.alt };
};
const generateTangentialArc = (inbdCrs, outbCrs, arcStart, legEnd, turnDir) => {
const line = [];
if (inbdCrs !== outbCrs) {
// Course to the end of the arc
let crsToArcEnd;
if (!turnDir || turnDir === "E") {
let prov = outbCrs - inbdCrs;
prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov;
turnDir = prov > 0 ? "L" : "R";
}
if (turnDir === "R") {
const delta = (360 - outbCrs + inbdCrs).normaliseDegrees();
crsToArcEnd = (outbCrs + delta / 2).normaliseDegrees();
} else {
const delta = (outbCrs + 360 - inbdCrs).normaliseDegrees();
crsToArcEnd = (outbCrs - delta / 2).normaliseDegrees();
}
// Arc end
const arcEnd = computeIntersection(
arcStart,
crsToArcEnd,
legEnd,
inbdCrs.flipCourse()
);
if (!arcEnd) {
// Early end due to no intercept
return null;
}
let startPerpCrs;
let endPerpCrs;
if (turnDir === "R") {
startPerpCrs = (outbCrs + 90).normaliseDegrees();
endPerpCrs = (inbdCrs + 90).normaliseDegrees();
} else {
startPerpCrs = (outbCrs - 90).normaliseDegrees();
endPerpCrs = (inbdCrs - 90).normaliseDegrees();
}
// Generate arc
const arcCenter = computeIntersection(
arcStart,
startPerpCrs,
arcEnd,
endPerpCrs
);
const arcRad = geolib.getDistance(
{
latitude: arcCenter.lat,
longitude: arcCenter.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(
arcCenter,
arcRad,
startPerpCrs
);
line.push([arcFix.longitude, arcFix.latitude]);
}
}
return line;
};
const generatePerformanceArc = (inbdCrs, outbCrs, arcStart, 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";
}
// Generate arc
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(
{
latitude: line.at(-1)[1],
longitude: line.at(-1)[0],
},
nmiToMetre(240 / 3600),
outbCrs
);
line.push([arcFix.longitude, arcFix.latitude]);
}
} else {
line.push([arcStart.lon, arcStart.lat]);
}
return line.slice(1);
};
const getCourseAndFixForInterceptions = (leg) => {
switch (leg.TrackCode) {
case "CF": {
return [
leg.Course.flipCourse().toTrue(),
{ lat: leg.WptLat, lon: leg.WptLon },
];
}
case "FM": {
return [leg.Course.toTrue(), { lat: leg.WptLat, lon: leg.WptLon }];
}
}
};
const handleTurnAtFix = (inbdCrs, inbdCrs2, start, end, turnDir) => {
// Begin line drawing at previous end
const line = [[start.lon, start.lat]];
// Draw arcs only if origin was flyover
if (start.isFlyOver) {
const arc1 = generateTangentialArc(
inbdCrs,
lastCourse,
start,
end,
turnDir
);
const arc2 = generatePerformanceArc(inbdCrs2, lastCourse, start, turnDir);
let arc;
if (arc1) {
const endCrs = geolib.getGreatCircleBearing(
{
latitude: arc1.at(-1)[1],
longitude: arc1.at(-1)[0],
},
{
latitude: end.lat,
longitude: end.lon,
}
);
if (endCrs <= inbdCrs + 1 && endCrs >= inbdCrs - 1) arc = arc1;
else arc = arc2;
} else {
arc = arc2;
}
// Push line
line.push(...arc);
} else {
//FIXME: Non Flyover
//line.push([end.lon, end.lat]);
}
return line;
};
const parseAltitude = (alt) => {
return Number.parseInt(alt.substring(0, 5));
};
const SPEED = 4 / 60; //nmi/s
const TURN = 3; //deg/s
const VS = 1; //fps
// ADJ to location, (NG), E is neg.
const MAGVAR = -1;
const PROCEDURE_ID = 10394;
/* 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 points = [{ lat: runway.Latitude, lon: runway.Longitude }];
/* Output */
const points = [
{ lat: runway.Latitude, lon: runway.Longitude, alt: runway.Elevation },
];
const lines = [];
let lastCourse = 0;
procedure.forEach((leg) => {
/* Main */
for (let index = 0; index < procedure.length; index++) {
const leg = procedure[index];
const waypoint = waypoints.filter(({ ID }) => ID === leg.WptID)[0];
switch (leg.TrackCode) {
@ -121,160 +349,315 @@ procedure.forEach((leg) => {
case "CD":
break;
case "CF": {
// 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,
});
if (leg.TurnDir) {
const line = [
[points[points.length - 2].lon, points[points.length - 2].lat],
];
let inbdCrs = Math.round(
geolib.getGreatCircleBearing(
{
latitude: points[points.length - 3].lat,
longitude: points[points.length - 3].lon,
},
{
latitude: points[points.length - 2].lat,
longitude: points[points.length - 2].lon,
}
)
);
let outbCrs = Math.round(leg.Course - MAGVAR);
outbCrs =
outbCrs >= 360
? outbCrs - 360
: outbCrs < 0
? outbCrs + 360
: outbCrs;
const delta = Math.abs(inbdCrs - 180 - outbCrs);
let perpCrs;
if (leg.TurnDir === "R") {
perpCrs = inbdCrs + 90 + delta / 2;
perpCrs = perpCrs >= 360 ? perpCrs - 360 : perpCrs;
} else {
perpCrs = inbdCrs - 90 - delta / 2;
perpCrs = perpCrs < 0 ? perpCrs + 360 : perpCrs;
}
const perpFix = intersection(
points[points.length - 2],
Math.round(perpCrs - MAGVAR),
points[points.length - 1],
Math.round(leg.Course + 180 - MAGVAR)
);
const midFix = geolib.getCenter([
{
latitude: points[points.length - 2].lat,
longitude: points[points.length - 2].lon,
},
{
latitude: perpFix.lat,
longitude: perpFix.lon,
},
]);
const dist = geolib.getDistance(
{
latitude: points[points.length - 2].lat,
longitude: points[points.length - 2].lon,
},
{
latitude: perpFix.lat,
longitude: perpFix.lon,
}
);
const mid1 = geolib.computeDestinationPoint(midFix, dist / 2, inbdCrs);
let invCrs = outbCrs + 180;
invCrs = invCrs >= 360 ? invCrs - 360 : invCrs;
const mid2 = geolib.computeDestinationPoint(midFix, dist / 2, invCrs);
line.push([mid1.longitude, mid1.latitude]);
line.push([mid2.longitude, mid2.latitude]);
line.push([perpFix.lon, perpFix.lat]);
line.push([
points[points.length - 1].lon,
points[points.length - 1].lat,
]);
lines.push({ line });
} else {
lines.push({
line: [
[points[points.length - 2].lon, points[points.length - 2].lat],
[points[points.length - 1].lon, points[points.length - 1].lat],
],
});
}
break;
}
case "CI":
break;
case "CR": {
const intc = intersection(
points[points.length - 1],
Math.round(leg.Course - MAGVAR),
{ lat: leg.NavLat, lon: leg.NavLon },
Math.round(leg.NavBear - MAGVAR)
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
points.at(-2),
points.at(-1),
leg.TurnDir,
index
);
line.push([leg.WptLon, leg.WptLat]);
lines.push({ line });
updateLastCourse(lines.at(-1).line);
break;
}
case "CI": {
// Course into the destination fix and said fix
const [inbdCrs, fix] = getCourseAndFixForInterceptions(
procedure[index + 1]
);
// Compute INTC
const intc = computeIntersection(
points.at(-1),
leg.Course.toTrue(),
fix,
inbdCrs
);
const line = handleTurnAtFix(
inbdCrs,
leg.Course.toTrue(),
points.at(-1),
intc,
leg.TurnDir,
index
);
lines.push({ line });
updateLastCourse(line);
const intc2 = computeIntersection(
{ lat: line.at(-1)[1], lon: line.at(-1)[0] },
leg.Course.toTrue(),
fix,
inbdCrs
);
if (intc2) {
points.push(intc2);
lines.push({ line: [line.at(-1), [intc2.lon, intc2.lat]] });
updateLastCourse(line);
} else {
points.push(intc);
lines.push({
line: [line.at(-1), [intc.lon, intc.lat]],
});
}
break;
}
case "CR": {
// Course into the destination fix
const inbdCrs = leg.Course.toTrue();
// Compute INTC
const intc = computeIntersection(
points.at(-1),
inbdCrs,
{ lat: leg.NavLat, lon: leg.NavLon },
leg.NavBear.toTrue()
);
points.push(intc);
lines.push({
line: [
[points[points.length - 2].lon, points[points.length - 2].lat],
[points[points.length - 1].lon, points[points.length - 1].lat],
],
});
const line = handleTurnAtFix(
inbdCrs,
leg.Course.toTrue(),
points.at(-2),
points.at(-1),
leg.TurnDir,
index
);
line.push([intc.lon, intc.lat]);
lines.push({ line });
updateLastCourse(line);
break;
}
case "DF":
case "FA":
case "FC":
case "FD":
case "FM":
break;
case "FM": {
const end = geolib.computeDestinationPoint(
{
latitude: points.at(-1).lat,
longitude: points.at(-1).lon,
},
nmiToMetre(10),
leg.Course.toTrue()
);
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
points.at(-1),
{ lat: end.latitude, lon: end.longitude },
leg.TurnDir,
index
);
lines.push({ line });
line.push([end.longitude, end.latitude]);
updateLastCourse(line);
break;
}
case "HA":
case "HF":
case "HM":
case "IF":
case "PI":
case "RF":
case "TF":
case "VA":
case "VD":
case "VI":
break;
case "TF": {
// 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 = geolib.getGreatCircleBearing(
{ latitude: points.at(-2).lat, longitude: points.at(-2).lon },
{ latitude: points.at(-1).lat, longitude: points.at(-1).lon }
);
const line = handleTurnAtFix(
inbdCrs,
inbdCrs,
points.at(-2),
points.at(-1),
leg.TurnDir,
index
);
line.push([leg.WptLon, leg.WptLat]);
lines.push({ line });
updateLastCourse(line);
break;
}
case "VA": {
// NOTE: No wind adjustments to be made, no clue how *that* would draw
const end = geolib.computeDestinationPoint(
{
latitude: points.at(-1).lat,
longitude: points.at(-1).lon,
},
nmiToMetre(
((parseAltitude(leg.Alt) - points.at(-1).alt) / AC_VS) *
(AC_SPEED / 60)
),
leg.Course.toTrue()
);
points.push({
lat: end.latitude,
lon: end.longitude,
name: leg.Alt,
"marker-color": "#ff0000",
isFlyOver: true,
alt: parseAltitude(leg.Alt),
});
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
points.at(-2),
{ lat: end.latitude, lon: end.longitude },
leg.TurnDir,
index
);
line.push([end.longitude, end.latitude]);
lines.push({ line });
updateLastCourse(line);
break;
}
case "VD": {
// NOTE: No wind adjustments to be made, no clue how *that* would draw
const end = geolib.computeDestinationPoint(
{
latitude: points.at(-1).lat,
longitude: points.at(-1).lon,
},
//NOTE: Does not account for slant
nmiToMetre(leg.Distance),
leg.Course.toTrue()
);
points.push({
lat: end.latitude,
lon: end.longitude,
name: leg.Alt,
"marker-color": "#ff0000",
isFlyOver: true,
alt: points.at(-1).alt,
});
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
points.at(-2),
{ lat: end.latitude, lon: end.longitude },
leg.TurnDir,
index
);
line.push([end.longitude, end.latitude]);
lines.push({ line });
updateLastCourse(line);
break;
}
case "VI": {
// NOTE: No wind adjustments to be made, no clue how *that* would draw
// Course into the destination fix and said fix
const [inbdCrs, fix] = getCourseAndFixForInterceptions(
procedure[index + 1]
);
// Compute INTC
const intc = computeIntersection(
points.at(-1),
leg.Course.toTrue(),
fix,
inbdCrs
);
const line = handleTurnAtFix(
inbdCrs,
leg.Course.toTrue(),
points.at(-1),
intc,
leg.TurnDir,
index
);
lines.push({ line });
updateLastCourse(line);
const intc2 = computeIntersection(
{ lat: line.at(-1)[1], lon: line.at(-1)[0] },
leg.Course.toTrue(),
fix,
inbdCrs
);
if (intc2) {
points.push(intc2);
lines.push({ line: [line.at(-1), [intc2.lon, intc2.lat]] });
updateLastCourse(line);
}
break;
}
case "VM": {
const end = geolib.computeDestinationPoint(
{
latitude: points[points.length - 1].lat,
longitude: points[points.length - 1].lon,
latitude: points.at(-1).lat,
longitude: points.at(-1).lon,
},
nmiToMetre(10),
leg.Course - MAGVAR
leg.Course.toTrue()
);
lines.push({
line: [
[points[points.length - 1].lon, points[points.length - 1].lat],
[end.longitude, end.latitude],
],
});
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
points.at(-1),
{ lat: end.latitude, lon: end.longitude },
leg.TurnDir,
index
);
line.push([end.longitude, end.latitude]);
lines.push({ line });
updateLastCourse(line);
break;
}
case "VR":
case "AF":
default:
break;
}
});
}
/* geoJSON */
const output = geo.parse([...points, ...lines], {
Point: ["lat", "lon"],
LineString: "line",
});
console.log(JSON.stringify(output, null, 2));

View File

@ -1,8 +1,11 @@
{
"dependencies": {
"geojson": "^0.5.0",
"geolib": "^3.3.4",
"magvar": "^2.0.0"
},
"type": "module"
"dependencies": {
"geojson": "^0.5.0",
"geolib": "^3.3.4",
"magvar": "^2.0.0"
},
"type": "module",
"scripts": {
"parse": "node geojson.js > output.json"
}
}