Refactor complete

This commit is contained in:
Kilian Hofmann 2025-07-14 03:31:57 +02:00
parent 2bdbb583a8
commit af7ac30926
40 changed files with 986 additions and 1013 deletions

View File

@ -1,3 +0,0 @@
{
"cSpell.words": ["intc"]
}

View File

@ -176,7 +176,7 @@ LGAV KOR1D SID (Cycle 2507, ID 10679)
### Instructions
- From preceding fix, fly `Course` until interception of `NavBear` radial *from* the navaid
identified by `NavID`.
identified by (`NavID`, `NavLat`, `NavLon`).
### Units
@ -307,7 +307,7 @@ 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)
## Track from Fix to Manual Termination (FM)
### Example
@ -545,7 +545,7 @@ LFRN GODA5R SID (cycle 2507, ID 10485)
### Instructions
- From the preceding waypoint, fly direct to the waypoint identified by
`WptID`, `WptLat`, `WptLon`.
(`WptID`, `WptLat`, `WptLon`).
### Units
@ -595,7 +595,7 @@ LFRK NEVI4Y SID (Cycle 2507, ID 10482)
### Instructions
- From preceding fix, fly heading specified in `Course` until reaching `Distance` from the navaid
identified by `NavID`.
identified by (`NavID`, `NavLat`, `NavLon`).
- **No wind corrections shall be applied**
### Units

View File

@ -20,9 +20,6 @@ importers:
magvar:
specifier: ^2.0.0
version: 2.0.0
object-hash:
specifier: ^3.0.0
version: 3.0.0
react:
specifier: 19.0.0-rc.1
version: 19.0.0-rc.1
@ -63,6 +60,9 @@ importers:
globals:
specifier: ^16.3.0
version: 16.3.0
object-hash:
specifier: ^3.0.0
version: 3.0.0
typescript:
specifier: ~5.8.3
version: 5.8.3

View File

@ -1,43 +1,114 @@
import { MapContainer, GeoJSON, TileLayer } from "react-leaflet";
import Parser from "./parser/parser";
import { useEffect, useState } from "react";
import { createRef, useEffect, useState } from "react";
import hash from "object-hash";
import Leaflet from "leaflet";
import L from "leaflet";
const parser = await Parser.instance();
const terminals = [10394, 10395, 10475, 10480, 10482, 10485, 10653];
function App() {
const [selectedTerminal, setSelectedTerminal] = useState(terminals[0]);
const [procedure, setProcedure] = useState<string>();
console.log(procedure);
const mapRef = createRef<Leaflet.Map>();
const layerRef = createRef<Leaflet.GeoJSON>();
useEffect(() => {
(async () => {
setProcedure(await parser.parse(10394));
setProcedure(await parser.parse(selectedTerminal));
})();
}, []);
}, [selectedTerminal]);
useEffect(() => {
if (layerRef.current && mapRef.current) {
mapRef.current.flyToBounds(layerRef.current.getBounds(), {
animate: false,
padding: [50, 50],
});
}
});
return (
<div style={{ display: "flex", height: "100vh", width: "100vw" }}>
<MapContainer
center={[51.505, -0.09]}
zoom={13}
zoomSnap={0}
zoomDelta={0.1}
wheelPxPerZoomLevel={1000}
style={{ height: "100%", width: "100%" }}
ref={mapRef}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<GeoJSON
key={hash(procedure ?? "")}
key={hash(procedure ?? "") + "lines"}
data={procedure}
style={{
color: "#00ffff",
stroke: true,
weight: 2,
weight: 5,
opacity: 1,
}}
filter={(feature) => feature.geometry.type !== "Point"}
ref={layerRef}
/>
<GeoJSON
key={hash(procedure ?? "") + "points"}
data={procedure}
style={(feature) => ({
color: feature.properties["marker-color"],
stroke: false,
fill: true,
fillOpacity: 1,
})}
pointToLayer={(_, latlng) => {
return L.circleMarker(latlng, { radius: 5 });
}}
onEachFeature={({ geometry, properties }, layer) => {
if (geometry.type === "Point") {
layer.bindPopup(
`${properties.name}<br>
${properties.altitude} ft<br>
${properties.speed} kts<br>
CNSTR:
${properties.altitudeConstraint ?? ""}
${properties.speedConstraint ?? ""}<br>`
);
}
}}
filter={(feature) => feature.geometry.type === "Point"}
/>
</MapContainer>
<div></div>
<div
style={{
padding: "5px",
display: "flex",
flexDirection: "column",
gap: "10px",
}}
>
{terminals.map((terminal) => (
<div
key={terminal}
style={{
display: "flex",
gap: "10px",
background: selectedTerminal === terminal ? "#eeeeee" : undefined,
}}
>
<pre>ID {terminal}</pre>
<button onClick={() => setSelectedTerminal(terminal)}>
Select
</button>
</div>
))}
</div>
</div>
);
}

View File

@ -13,4 +13,4 @@ fetch = async (path: string) => {
const parser = await Parser.instance();
console.log(JSON.stringify(await parser.parse(10394)));
console.log(JSON.stringify(await parser.parse(10480)));

View File

@ -1,7 +1,17 @@
import "./utils/extensions.ts";
import { handleTurnAtFix } from "./utils/handleTurnAtFix.ts";
import * as geolib from "geolib";
import geojson from "geojson";
import { TerminatorsCF } from "./terminators/CF.ts";
import { TerminatorsAF } from "./terminators/AF.ts";
import { TerminatorsCR } from "./terminators/CR.ts";
import { TerminatorsVM } from "./terminators/VM.ts";
import { TerminatorsFM } from "./terminators/FM.ts";
import { TerminatorsCI } from "./terminators/CI.ts";
import { TerminatorsVA } from "./terminators/VA.ts";
import { TerminatorsTF } from "./terminators/TF.ts";
import { TerminatorsVI } from "./terminators/VI.ts";
import { TerminatorsVD } from "./terminators/VD.ts";
import { TerminatorsRF } from "./terminators/RF.ts";
class Parser {
private static _instance: Parser;
@ -11,6 +21,7 @@ class Parser {
private terminals: Terminal[];
public static AC_SPEED = 250;
public static AC_VS = 1500;
private constructor(
waypoints: Waypoint[],
@ -84,53 +95,172 @@ class Parser {
const waypoint = this.waypoints.filter(({ ID }) => ID === leg.WptID)[0];
switch (leg.TrackCode) {
case "AF":
case "CA":
case "CD":
break;
case "CF": {
const _leg = leg as CFTerminalEntry;
const targetFix: NavFix = {
latitude: _leg.WptLat,
longitude: _leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": _leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: _leg.IsFlyOver,
altitude: previousFix.altitude,
};
navFixes.push(targetFix);
const line = handleTurnAtFix(
_leg.Course.toTrue(previousFix),
_leg.Course.toTrue(previousFix),
lastCourse,
case "AF": {
const [fixToAdd, lineToAdd] = TerminatorsAF(
leg as AFTerminalEntry,
previousFix,
targetFix,
_leg.TurnDir
waypoint
);
lineSegments.push({ line });
updateLastCourse(lineSegments.at(-1)!.line);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "CA":
case "CD":
console.error("Unknown TrackCode", leg.TrackCode);
break;
case "CF": {
const [fixToAdd, lineToAdd] = TerminatorsCF(
leg as CFTerminalEntry,
previousFix,
lastCourse,
waypoint
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "CI": {
const [fixToAdd, lineToAdd] = TerminatorsCI(
leg as CITerminalEntry,
procedure[index + 1],
previousFix,
lastCourse
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "CR": {
const [fixToAdd, lineToAdd] = TerminatorsCR(
leg as CRTerminalEntry,
previousFix,
lastCourse
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "CI":
case "CR":
case "DF":
case "FA":
case "FC":
case "FD":
case "FM":
console.error("Unknown TrackCode", leg.TrackCode);
break;
case "FM": {
const [fixToAdd, lineToAdd] = TerminatorsFM(
leg as FMTerminalEntry,
previousFix,
lastCourse
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "HA":
case "HF":
case "HM":
case "IF":
case "PI":
case "RF":
case "TF":
case "VA":
case "VD":
case "VI":
case "VM":
console.error("Unknown TrackCode", leg.TrackCode);
break;
case "RF": {
const [fixToAdd, lineToAdd] = TerminatorsRF(
leg as RFTerminalEntry,
procedure[index + 1],
previousFix,
lastCourse,
waypoint
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "TF": {
const [fixToAdd, lineToAdd] = TerminatorsTF(
leg as TFTerminalEntry,
previousFix,
lastCourse,
waypoint
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "VA": {
const [fixToAdd, lineToAdd] = TerminatorsVA(
leg as VATerminalEntry,
previousFix,
lastCourse
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "VD": {
const [fixToAdd, lineToAdd] = TerminatorsVD(
leg as VDTerminalEntry,
previousFix,
lastCourse
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "VI": {
const [fixToAdd, lineToAdd] = TerminatorsVI(
leg as VITerminalEntry,
procedure[index + 1],
previousFix,
lastCourse
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "VM": {
const [fixToAdd, lineToAdd] = TerminatorsVM(
leg as VMTerminalEntry,
previousFix,
lastCourse
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
updateLastCourse(lineToAdd);
}
break;
}
case "VR":
default:
console.error("Unknown TrackCode", leg.TrackCode);
@ -139,8 +269,8 @@ class Parser {
}
return geojson.parse([...navFixes, ...lineSegments], {
Point: ["latitude", "longitude"],
LineString: "line",
Point: ["latitude", "longitude"],
});
};
}

View File

@ -0,0 +1,53 @@
import * as geolib from "geolib";
/**
* @param crsIntoEndpoint Course into arc endpoint
* @param crsFromOrigin Course into arc origin point
* @param start Arc origin point
* @param center Arc center point
* @param radius Arc radius in nmi
* @param turnDir
* @returns Line segments
*/
export const generateAFArc = (
crsIntoEndpoint: number,
crsFromOrigin: number,
start: NavFix,
center: NavFix,
radius: number,
turnDir: TurnDirection
) => {
const line: LineSegment[] = [[start.longitude, start.latitude]];
if (crsIntoEndpoint !== crsFromOrigin) {
// Turn Dir
if (!turnDir || turnDir === "E") {
let prov = crsFromOrigin - crsIntoEndpoint;
prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov;
turnDir = prov > 0 ? "L" : "R";
}
while (crsFromOrigin !== crsIntoEndpoint) {
if (turnDir === "R") {
const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees();
crsFromOrigin += delta < 1 ? delta : 1;
crsFromOrigin = crsFromOrigin.normaliseDegrees();
} else {
const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees();
crsFromOrigin -= delta < 1 ? delta : 1;
crsFromOrigin = crsFromOrigin.normaliseDegrees();
}
if (crsFromOrigin === crsIntoEndpoint) break;
const arcFix = geolib.computeDestinationPoint(
center,
radius.toMetre(),
crsFromOrigin
);
line.push([arcFix.longitude, arcFix.latitude]);
}
}
return line;
};

View File

@ -0,0 +1,77 @@
import * as geolib from "geolib";
/**
* @param crsIntoEndpoint Course into arc endpoint
* @param crsIntoOrigin Course into arc origin point
* @param start Arc origin point
* @param center Arc center point
* @param turnDir
* @returns Line segments
*/
export const generateRFArc = (
crsIntoEndpoint: number,
crsIntoOrigin: number,
start: NavFix,
center: NavFix,
turnDir: TurnDirection
) => {
const line: LineSegment[] = [[start.longitude, start.latitude]];
if (crsIntoEndpoint !== crsIntoOrigin) {
// Turn Dir
if (!turnDir || turnDir === "E") {
let prov = crsIntoOrigin - crsIntoEndpoint;
prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov;
turnDir = prov > 0 ? "L" : "R";
}
let crsOrthogonalOnOrigin;
let crsOrthogonalOnEndpoint;
if (turnDir === "R") {
crsOrthogonalOnOrigin = (crsIntoOrigin + 90).normaliseDegrees();
crsOrthogonalOnEndpoint = (crsIntoEndpoint + 90).normaliseDegrees();
} else {
crsOrthogonalOnOrigin = (crsIntoOrigin - 90).normaliseDegrees();
crsOrthogonalOnEndpoint = (crsIntoEndpoint - 90).normaliseDegrees();
}
const arcRad = geolib.getDistance(center, start);
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse();
crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse();
// Start turn immediately
if (turnDir === "R") {
crsOrthogonalOnOrigin +=
crsOrthogonalOnOrigin < 1 ? crsOrthogonalOnOrigin : 1;
} else {
crsOrthogonalOnOrigin -=
crsOrthogonalOnOrigin < 1 ? crsOrthogonalOnOrigin : 1;
}
while (crsOrthogonalOnOrigin !== crsOrthogonalOnEndpoint) {
if (turnDir === "R") {
const delta = (
crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin
).normaliseDegrees();
crsOrthogonalOnOrigin += delta < 1 ? delta : 1;
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees();
} else {
const delta = (
crsOrthogonalOnOrigin - crsOrthogonalOnEndpoint
).normaliseDegrees();
crsOrthogonalOnOrigin -= delta < 1 ? delta : 1;
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees();
}
const arcFix = geolib.computeDestinationPoint(
center,
arcRad,
crsOrthogonalOnOrigin
);
line.push([arcFix.longitude, arcFix.latitude]);
}
}
return line;
};

View File

@ -1,5 +1,5 @@
import * as geolib from "geolib";
import { computeIntersection } from "./computeIntersection.ts";
import { computeIntersection } from "../utils/computeIntersection.ts";
/**
* @param crsIntoEndpoint Course into arc endpoint

View File

@ -1 +1,48 @@
export const TerminatorsAF = () => {};
import * as geolib from "geolib";
import { generateAFArc } from "../pathGenerators/generateAFArc.ts";
import Parser from "../parser.ts";
export const TerminatorsAF = (
leg: AFTerminalEntry,
previousFix: NavFix,
waypoint?: Waypoint
): [NavFix?, LineSegment[]?] => {
const targetFix: NavFix = {
latitude: leg.WptLat,
longitude: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit
? leg.SpeedLimit > Parser.AC_SPEED
? Parser.AC_SPEED
: leg.SpeedLimit
: previousFix.speed
? previousFix.speed
: Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
const arcEndCrs = geolib.getGreatCircleBearing(
{
latitude: leg.NavLat,
longitude: leg.NavLon,
},
{
latitude: leg.WptLat,
longitude: leg.WptLon,
}
);
const line = generateAFArc(
arcEndCrs,
leg.Course.toTrue({ latitude: leg.NavLat, longitude: leg.NavLon }),
previousFix,
{ latitude: leg.NavLat, longitude: leg.NavLon },
leg.NavDist,
leg.TurnDir
);
return [targetFix, line];
};

View File

@ -0,0 +1,38 @@
import Parser from "../parser.ts";
import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts";
export const TerminatorsCF = (
leg: CFTerminalEntry,
previousFix: NavFix,
lastCourse: number,
waypoint?: Waypoint
): [NavFix?, LineSegment[]?] => {
const targetFix: NavFix = {
latitude: leg.WptLat,
longitude: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit
? leg.SpeedLimit > Parser.AC_SPEED
? Parser.AC_SPEED
: leg.SpeedLimit
: previousFix.speed
? previousFix.speed
: Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
const line = handleTurnAtFix(
leg.Course.toTrue(previousFix),
leg.Course.toTrue(previousFix),
lastCourse,
previousFix,
targetFix,
leg.TurnDir
);
return [targetFix, line];
};

View File

@ -0,0 +1,62 @@
import Parser from "../parser.ts";
import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts";
import { computeIntersection } from "../utils/computeIntersection.ts";
import { getCourseAndFixForIntercepts } from "../utils/getCourseAndFixForIntercepts.ts";
export const TerminatorsCI = (
leg: CITerminalEntry,
nextLeg: TerminalEntry,
previousFix: NavFix,
lastCourse: number
): [NavFix?, LineSegment[]?] => {
const [crs, nextFix] = getCourseAndFixForIntercepts(nextLeg, previousFix);
// Compute INTC
const interceptFix: NavFix = {
...computeIntersection(
previousFix,
leg.Course.toTrue(nextFix),
nextFix,
crs
)!,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit
? leg.SpeedLimit > Parser.AC_SPEED
? Parser.AC_SPEED
: leg.SpeedLimit
: previousFix.speed
? previousFix.speed
: Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
const line = handleTurnAtFix(
crs,
leg.Course.toTrue(nextFix),
lastCourse,
previousFix,
interceptFix,
leg.TurnDir
);
// Intercept based on previous intercept
const interceptPoint2 = computeIntersection(
{ latitude: line.at(-2)![1], longitude: line.at(-2)![0] },
leg.Course.toTrue(nextFix),
nextFix,
crs
);
if (interceptPoint2)
return [
{ ...interceptFix, ...interceptPoint2 },
[
...line.slice(0, -1),
[interceptPoint2.longitude, interceptPoint2.latitude],
],
];
return [interceptFix, line];
};

View File

@ -0,0 +1,43 @@
import { computeIntersection } from "../utils/computeIntersection.ts";
import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts";
import Parser from "../parser.ts";
export const TerminatorsCR = (
leg: CRTerminalEntry,
previousFix: NavFix,
lastCourse: number
): [NavFix?, LineSegment[]?] => {
const crsIntoEndpoint = leg.Course.toTrue(previousFix);
const interceptFix: NavFix = {
...computeIntersection(
previousFix,
crsIntoEndpoint,
{ latitude: leg.NavLat, longitude: leg.NavLon },
leg.NavBear.toTrue({ latitude: leg.NavLat, longitude: leg.NavLon })
)!,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit
? leg.SpeedLimit > Parser.AC_SPEED
? Parser.AC_SPEED
: leg.SpeedLimit
: previousFix.speed
? previousFix.speed
: Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
const line = handleTurnAtFix(
crsIntoEndpoint,
leg.Course.toTrue(previousFix),
lastCourse,
previousFix,
interceptFix,
leg.TurnDir
);
return [interceptFix, line];
};

View File

@ -0,0 +1,25 @@
import * as geolib from "geolib";
import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts";
export const TerminatorsFM = (
leg: FMTerminalEntry,
previousFix: NavFix,
lastCourse: number
): [NavFix?, LineSegment[]?] => {
const endpoint = geolib.computeDestinationPoint(
previousFix,
(10).toMetre(),
leg.Course.toTrue(previousFix)
);
const line = handleTurnAtFix(
leg.Course.toTrue(previousFix),
leg.Course.toTrue(previousFix),
lastCourse,
previousFix,
endpoint,
leg.TurnDir
);
return [undefined, line];
};

View File

@ -0,0 +1,42 @@
import Parser from "../parser.ts";
import { getCourseAndFixForIntercepts } from "../utils/getCourseAndFixForIntercepts.ts";
import { generateRFArc } from "../pathGenerators/generateRFArc.ts";
export const TerminatorsRF = (
leg: RFTerminalEntry,
nextLeg: TerminalEntry,
previousFix: NavFix,
lastCourse: number,
waypoint?: Waypoint
): [NavFix?, LineSegment[]?] => {
const targetFix: NavFix = {
latitude: leg.WptLat,
longitude: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit
? leg.SpeedLimit > Parser.AC_SPEED
? Parser.AC_SPEED
: leg.SpeedLimit
: previousFix.speed
? previousFix.speed
: Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
const [crs] = getCourseAndFixForIntercepts(nextLeg, previousFix);
const line = generateRFArc(
crs,
lastCourse,
previousFix,
{ latitude: leg.CenterLat, longitude: leg.CenterLon },
leg.TurnDir
);
line.push([targetFix.longitude, targetFix.latitude]);
return [targetFix, line];
};

View File

@ -0,0 +1,40 @@
import * as geolib from "geolib";
import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts";
import Parser from "../parser.ts";
export const TerminatorsTF = (
leg: TFTerminalEntry,
previousFix: NavFix,
lastCourse: number,
waypoint?: Waypoint
): [NavFix?, LineSegment[]?] => {
const targetFix: NavFix = {
latitude: leg.WptLat,
longitude: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit
? leg.SpeedLimit > Parser.AC_SPEED
? Parser.AC_SPEED
: leg.SpeedLimit
: previousFix.speed
? previousFix.speed
: Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
const crs = geolib.getGreatCircleBearing(previousFix, targetFix);
const line = handleTurnAtFix(
crs,
crs,
lastCourse,
previousFix,
targetFix,
leg.TurnDir
);
return [targetFix, line];
};

View File

@ -0,0 +1,46 @@
import * as geolib from "geolib";
import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts";
import Parser from "../parser.ts";
// NOTE: No wind adjustments to be made, no clue how *that* would draw
export const TerminatorsVA = (
leg: VATerminalEntry,
previousFix: NavFix,
lastCourse: number
): [NavFix?, LineSegment[]?] => {
const targetFix: NavFix = {
...geolib.computeDestinationPoint(
previousFix,
(
((leg.Alt.parseAltitude() - (previousFix.altitude ?? 0)) /
Parser.AC_VS) *
(Parser.AC_SPEED / 60)
).toMetre(),
leg.Course.toTrue(previousFix)
),
name: leg.Alt,
"marker-color": "#ff0000",
isFlyOver: true,
altitude: leg.Alt.parseAltitude(),
speed: leg.SpeedLimit
? leg.SpeedLimit > Parser.AC_SPEED
? Parser.AC_SPEED
: leg.SpeedLimit
: previousFix.speed
? previousFix.speed
: Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
const line = handleTurnAtFix(
leg.Course.toTrue(previousFix),
leg.Course.toTrue(previousFix),
lastCourse,
previousFix,
targetFix,
leg.TurnDir
);
return [targetFix, line];
};

View File

@ -0,0 +1,42 @@
import * as geolib from "geolib";
import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts";
import Parser from "../parser.ts";
// NOTE: No wind adjustments to be made, no clue how *that* would draw
export const TerminatorsVD = (
leg: VDTerminalEntry,
previousFix: NavFix,
lastCourse: number
): [NavFix?, LineSegment[]?] => {
const targetFix: NavFix = {
...geolib.computeDestinationPoint(
previousFix,
leg.Distance.toMetre(),
leg.Course.toTrue(previousFix)
),
name: leg.Distance.toString(),
"marker-color": "#ff0000",
isFlyOver: true,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit
? leg.SpeedLimit > Parser.AC_SPEED
? Parser.AC_SPEED
: leg.SpeedLimit
: previousFix.speed
? previousFix.speed
: Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
const line = handleTurnAtFix(
leg.Course.toTrue(previousFix),
leg.Course.toTrue(previousFix),
lastCourse,
previousFix,
targetFix,
leg.TurnDir
);
return [targetFix, line];
};

View File

@ -0,0 +1,63 @@
import Parser from "../parser.ts";
import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts";
import { computeIntersection } from "../utils/computeIntersection.ts";
import { getCourseAndFixForIntercepts } from "../utils/getCourseAndFixForIntercepts.ts";
// NOTE: No wind adjustments to be made, no clue how *that* would draw
export const TerminatorsVI = (
leg: VITerminalEntry,
nextLeg: TerminalEntry,
previousFix: NavFix,
lastCourse: number
): [NavFix?, LineSegment[]?] => {
const [crs, nextFix] = getCourseAndFixForIntercepts(nextLeg, previousFix);
// Compute INTC
const interceptFix: NavFix = {
...computeIntersection(
previousFix,
leg.Course.toTrue(nextFix),
nextFix,
crs
)!,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit
? leg.SpeedLimit > Parser.AC_SPEED
? Parser.AC_SPEED
: leg.SpeedLimit
: previousFix.speed
? previousFix.speed
: Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
const line = handleTurnAtFix(
crs,
leg.Course.toTrue(nextFix),
lastCourse,
previousFix,
interceptFix,
leg.TurnDir
);
// Intercept based on previous intercept
const interceptPoint2 = computeIntersection(
{ latitude: line.at(-2)![1], longitude: line.at(-2)![0] },
leg.Course.toTrue(nextFix),
nextFix,
crs
);
if (interceptPoint2)
return [
{ ...interceptFix, ...interceptPoint2 },
[
...line.slice(0, -1),
[interceptPoint2.longitude, interceptPoint2.latitude],
],
];
return [interceptFix, line];
};

View File

@ -0,0 +1,26 @@
import * as geolib from "geolib";
import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts";
// NOTE: No wind adjustments to be made, no clue how *that* would draw
export const TerminatorsVM = (
leg: VMTerminalEntry,
previousFix: NavFix,
lastCourse: number
): [NavFix?, LineSegment[]?] => {
const endpoint = geolib.computeDestinationPoint(
previousFix,
(10).toMetre(),
leg.Course.toTrue(previousFix)
);
const line = handleTurnAtFix(
leg.Course.toTrue(previousFix),
leg.Course.toTrue(previousFix),
lastCourse,
previousFix,
endpoint,
leg.TurnDir
);
return [undefined, line];
};

View File

@ -10,7 +10,7 @@ export const computeIntersection = (
brng1: number,
p2: NavFix,
brng2: number
): NavFix | null => {
): NavFix | undefined => {
if (isNaN(brng1)) throw new TypeError(`invalid brng1 ${brng1}`);
if (isNaN(brng2)) throw new TypeError(`invalid brng2 ${brng2}`);
@ -54,8 +54,8 @@ export const computeIntersection = (
const α1 = θ13 - θ12; // angle 2-1-3
const α2 = θ21 - θ23; // angle 1-2-3
if (Math.sin(α1) == 0 && Math.sin(α2) == 0) return null; // infinite intersections
if (Math.sin(α1) * Math.sin(α2) < 0) return null; // ambiguous intersection (antipodal/360°)
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°)
const cosα3 =
-Math.cos(α1) * Math.cos(α2) + Math.sin(α1) * Math.sin(α2) * Math.cos(δ12);

View File

@ -25,3 +25,7 @@ Number.prototype.toTrue = function (fix) {
Number.prototype.toMetre = function () {
return (this as number) * 1852.0;
};
String.prototype.parseAltitude = function () {
return Number.parseInt(this.substring(0, 5));
};

View File

@ -0,0 +1,42 @@
import * as geolib from "geolib";
/**
* @param leg Leg to examine
* @param origin Origin of current leg
* @returns Adjusted course and fix
*/
export const getCourseAndFixForIntercepts = (
leg: TerminalEntry,
origin: NavFix
): [number, NavFix] => {
switch (leg.TrackCode) {
case "CF": {
const _leg = leg as CFTerminalEntry;
const fix = { latitude: _leg.WptLat, longitude: _leg.WptLon };
return [_leg.Course.reciprocalCourse().toTrue(fix), fix];
}
case "FM": {
const _leg = leg as FMTerminalEntry;
const fix = { latitude: _leg.WptLat, longitude: _leg.WptLon };
return [_leg.Course.toTrue(fix), fix];
}
case "TF": {
const _leg = leg as FMTerminalEntry;
return [
geolib.getGreatCircleBearing(origin, {
latitude: _leg.WptLat,
longitude: _leg.WptLon,
}),
{ latitude: _leg.WptLat, longitude: _leg.WptLon },
];
}
case "AF": {
const _leg = leg as AFTerminalEntry;
const fix = { latitude: _leg.WptLat, longitude: _leg.WptLon };
return [_leg.Course.reciprocalCourse().toTrue(fix), fix];
}
default: {
return [-1, origin];
}
}
};

View File

@ -26,4 +26,11 @@ export declare global {
*/
toMetre: () => number;
}
interface String {
/**
* @returns Value parsed from altitude constraint
*/
parseAltitude: () => number;
}
}

View File

@ -2,10 +2,10 @@ export declare global {
type AFTerminalEntry = Required<
Pick<
TerminalEntry,
| "TurnDir"
| "WptID"
| "WptLat"
| "WptLon"
| "TurnDir"
| "NavID"
| "NavLat"
| "NavLon"

4
browser/src/types/terminators/CI.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
export declare global {
type CITerminalEntry = Required<Pick<TerminalEntry, "Course">> &
TerminalEntry;
}

6
browser/src/types/terminators/CR.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
export declare global {
type CRTerminalEntry = Required<
Pick<TerminalEntry, "NavID" | "NavLat" | "NavLon" | "NavBear" | "Course">
> &
TerminalEntry;
}

17
browser/src/types/terminators/FM.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
export declare global {
type FMTerminalEntry = Required<
Pick<
TerminalEntry,
| "WptID"
| "WptLat"
| "WptLon"
| "NavID"
| "NavLat"
| "NavLon"
| "NavBear"
| "NavDist"
| "Course"
>
> &
TerminalEntry;
}

18
browser/src/types/terminators/RF.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
export declare global {
type RFTerminalEntry = Required<
Pick<
TerminalEntry,
| "WptID"
| "WptLat"
| "WptLon"
| "TurnDir"
| "NavBear"
| "Course"
| "Distance"
| "CenterID"
| "CenterLat"
| "CenterLon"
>
> &
TerminalEntry;
}

6
browser/src/types/terminators/TF.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
export declare global {
type TFTerminalEntry = Required<
Pick<TerminalEntry, "WptID" | "WptLat" | "WptLon" | "IsFlyOver">
> &
TerminalEntry;
}

4
browser/src/types/terminators/VA.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
export declare global {
type VATerminalEntry = Required<Pick<TerminalEntry, "Course" | "Alt">> &
TerminalEntry;
}

6
browser/src/types/terminators/VD.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
export declare global {
type VDTerminalEntry = Required<
Pick<TerminalEntry, "NavID" | "NavLat" | "NavLon" | "Course" | "Distance">
> &
TerminalEntry;
}

4
browser/src/types/terminators/VI.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
export declare global {
type VITerminalEntry = Required<Pick<TerminalEntry, "Course">> &
TerminalEntry;
}

4
browser/src/types/terminators/VM.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
export declare global {
type VMTerminalEntry = Required<Pick<TerminalEntry, "Course">> &
TerminalEntry;
}

View File

@ -63,7 +63,7 @@ export declare global {
CenterLat?: number;
CenterLon?: number;
IsFlyOver: boolean;
SpeedLimit?: string;
SpeedLimit?: number;
IsFAF: boolean;
IsMAP: boolean;
};
@ -84,7 +84,7 @@ export declare global {
isFlyOver?: boolean;
"marker-color"?: string;
altitudeConstraint?: string;
speedConstraint?: string;
speedConstraint?: number;
};
type LineSegment = [number, number];

View File

@ -1,901 +0,0 @@
import fs from "fs";
import geo from "geojson";
import geolib from "geolib";
import magvar from "magvar";
/* 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;
const MAGVAR = -5;
const PROCEDURE_ID = 10653;
/* 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 (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(
{
latitude: line.at(-2)[1],
longitude: line.at(-2)[0],
},
{
latitude: line.at(-1)[1],
longitude: line.at(-1)[0],
}
);
};
/**
*
* @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}`);
// see www.edwilliams.org/avform.htm#Intersection
const φ1 = p1.lat.toRadians(),
λ1 = p1.lon.toRadians();
const φ2 = p2.lat.toRadians(),
λ2 = p2.lon.toRadians();
const θ13 = Number(brng1).toRadians(),
θ23 = Number(brng2).toRadians();
const Δφ = φ2 - φ1,
Δλ = λ2 - λ1;
// angular distance p1-p2
const δ12 =
2 *
Math.asin(
Math.sqrt(
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2)
)
);
if (Math.abs(δ12) < Number.EPSILON) return p1; // coincident points
// initial/final bearings between points
const cosθa =
(Math.sin(φ2) - Math.sin(φ1) * Math.cos(δ12)) /
(Math.sin(δ12) * Math.cos(φ1));
const cosθb =
(Math.sin(φ1) - Math.sin(φ2) * Math.cos(δ12)) /
(Math.sin(δ12) * Math.cos(φ2));
const θa = Math.acos(Math.min(Math.max(cosθa, -1), 1)); // protect against rounding errors
const θb = Math.acos(Math.min(Math.max(cosθb, -1), 1)); // protect against rounding errors
const θ12 = Math.sin(λ2 - λ1) > 0 ? θa : 2 * π - θa;
const θ21 = Math.sin(λ2 - λ1) > 0 ? 2 * π - θb : θb;
const α1 = θ13 - θ12; // angle 2-1-3
const α2 = θ21 - θ23; // angle 1-2-3
if (Math.sin(α1) == 0 && Math.sin(α2) == 0) return null; // infinite intersections
if (Math.sin(α1) * Math.sin(α2) < 0) return null; // ambiguous intersection (antipodal/360°)
const cosα3 =
-Math.cos(α1) * Math.cos(α2) + Math.sin(α1) * Math.sin(α2) * Math.cos(δ12);
const δ13 = Math.atan2(
Math.sin(δ12) * Math.sin(α1) * Math.sin(α2),
Math.cos(α2) + Math.cos(α1) * cosα3
);
const φ3 = Math.asin(
Math.min(
Math.max(
Math.sin(φ1) * Math.cos(δ13) +
Math.cos(φ1) * Math.sin(δ13) * Math.cos(θ13),
-1
),
1
)
);
const Δλ13 = Math.atan2(
Math.sin(θ13) * Math.sin(δ13) * Math.cos(φ1),
Math.cos(δ13) - Math.sin(φ1) * Math.sin(φ3)
);
const λ3 = λ1 + Δλ13;
const lat = φ3.toDegrees();
const lon = λ3.toDegrees();
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 = [];
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;
};
/**
*
* @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]];
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);
};
/**
*
* @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 [
geolib.getGreatCircleBearing(
{ latitude: origin.lat, longitude: origin.lon },
{ latitude: leg.WptLat, longitude: leg.WptLon }
),
{ lat: leg.WptLat, lon: leg.WptLon },
];
}
case "AF": {
const fix = { lat: leg.WptLat, lon: leg.WptLon };
return [leg.Course.flipCourse().toTrue(fix), fix];
}
}
};
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));
};
/* Data */
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 = runway.TrueHeading;
/* 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) {
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;
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,
});
const line = handleTurnAtFix(
leg.Course.toTrue(points.at(-1)),
leg.Course.toTrue(points.at(-1)),
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],
points.at(-1)
);
// Compute INTC
const intc = computeIntersection(
points.at(-1),
leg.Course.toTrue(fix),
fix,
inbdCrs
);
const line = handleTurnAtFix(
inbdCrs,
leg.Course.toTrue(fix),
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),
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(points.at(-1));
// Compute INTC
const intc = computeIntersection(
points.at(-1),
inbdCrs,
{ lat: leg.NavLat, lon: leg.NavLon },
leg.NavBear.toTrue({ lat: leg.NavLat, lon: leg.NavLon })
);
points.push(intc);
const line = handleTurnAtFix(
inbdCrs,
leg.Course.toTrue(points.at(-2)),
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":
break;
case "FM": {
const end = geolib.computeDestinationPoint(
{
latitude: points.at(-1).lat,
longitude: points.at(-1).lon,
},
nmiToMetre(10),
leg.Course.toTrue(points.at(-1))
);
const line = handleTurnAtFix(
leg.Course.toTrue(points.at(-1)),
leg.Course.toTrue(points.at(-1)),
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":
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({
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.at(-1))
);
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(points.at(-2)),
leg.Course.toTrue(points.at(-2)),
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.at(-1))
);
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(points.at(-2)),
leg.Course.toTrue(points.at(-2)),
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],
points.at(-1)
);
// Compute INTC
const intc = computeIntersection(
points.at(-1),
leg.Course.toTrue(fix),
fix,
inbdCrs
);
const line = handleTurnAtFix(
inbdCrs,
leg.Course.toTrue(fix),
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),
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.at(-1).lat,
longitude: points.at(-1).lon,
},
nmiToMetre(10),
leg.Course.toTrue(points.at(-1))
);
const line = handleTurnAtFix(
leg.Course.toTrue(points.at(-1)),
leg.Course.toTrue(points.at(-1)),
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,11 +0,0 @@
{
"dependencies": {
"geojson": "^0.5.0",
"geolib": "^3.3.4",
"magvar": "^2.0.0"
},
"type": "module",
"scripts": {
"parse": "node geojson.js > output.json"
}
}

39
pnpm-lock.yaml generated
View File

@ -1,39 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
geojson:
specifier: ^0.5.0
version: 0.5.0
geolib:
specifier: ^3.3.4
version: 3.3.4
magvar:
specifier: ^2.0.0
version: 2.0.0
packages:
geojson@0.5.0:
resolution: {integrity: sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==}
engines: {node: '>= 0.10'}
geolib@3.3.4:
resolution: {integrity: sha512-EicrlLLL3S42gE9/wde+11uiaYAaeSVDwCUIv2uMIoRBfNJCn8EsSI+6nS3r4TCKDO6+RQNM9ayLq2at+oZQWQ==}
magvar@2.0.0:
resolution: {integrity: sha512-00LpSwEJZcnyX3VsdCM2CHSCvB+M6sVQTawLCB3J9oK7eEueNltNx9GFL4YR+/HIkLM+l8rFKiY77JTKnOf0jw==}
snapshots:
geojson@0.5.0: {}
geolib@3.3.4: {}
magvar@2.0.0: {}