diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index bded346..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cSpell.words": ["intc"] -} diff --git a/README.md b/README.md index 0f8e607..5a965af 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index 85ae983..b5ddc19 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -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 diff --git a/browser/src/App.tsx b/browser/src/App.tsx index da066e9..1d38d07 100644 --- a/browser/src/App.tsx +++ b/browser/src/App.tsx @@ -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(); - console.log(procedure); + + const mapRef = createRef(); + const layerRef = createRef(); 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 (
feature.geometry.type !== "Point"} + ref={layerRef} + /> + ({ + 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}
+ ${properties.altitude} ft
+ ${properties.speed} kts
+ CNSTR: + ${properties.altitudeConstraint ?? ""} + ${properties.speedConstraint ?? ""}
` + ); + } + }} + filter={(feature) => feature.geometry.type === "Point"} />
-
+
+ {terminals.map((terminal) => ( +
+
ID {terminal}
+ +
+ ))} +
); } diff --git a/browser/src/parser/node.ts b/browser/src/parser/node.ts index 84df73f..1a64471 100644 --- a/browser/src/parser/node.ts +++ b/browser/src/parser/node.ts @@ -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))); diff --git a/browser/src/parser/parser.ts b/browser/src/parser/parser.ts index a396bd7..fa629ea 100644 --- a/browser/src/parser/parser.ts +++ b/browser/src/parser/parser.ts @@ -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"], }); }; } diff --git a/browser/src/parser/pathGenerators/generateAFArc.ts b/browser/src/parser/pathGenerators/generateAFArc.ts new file mode 100644 index 0000000..d8fc2b8 --- /dev/null +++ b/browser/src/parser/pathGenerators/generateAFArc.ts @@ -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; +}; diff --git a/browser/src/parser/utils/generatePerformanceArc.ts b/browser/src/parser/pathGenerators/generatePerformanceArc.ts similarity index 100% rename from browser/src/parser/utils/generatePerformanceArc.ts rename to browser/src/parser/pathGenerators/generatePerformanceArc.ts diff --git a/browser/src/parser/pathGenerators/generateRFArc.ts b/browser/src/parser/pathGenerators/generateRFArc.ts new file mode 100644 index 0000000..62ba2d5 --- /dev/null +++ b/browser/src/parser/pathGenerators/generateRFArc.ts @@ -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; +}; diff --git a/browser/src/parser/utils/generateTangentArc.ts b/browser/src/parser/pathGenerators/generateTangentArc.ts similarity index 97% rename from browser/src/parser/utils/generateTangentArc.ts rename to browser/src/parser/pathGenerators/generateTangentArc.ts index e8b2a2d..bc42ae1 100644 --- a/browser/src/parser/utils/generateTangentArc.ts +++ b/browser/src/parser/pathGenerators/generateTangentArc.ts @@ -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 diff --git a/browser/src/parser/utils/handleTurnAtFix.ts b/browser/src/parser/pathGenerators/handleTurnAtFix.ts similarity index 100% rename from browser/src/parser/utils/handleTurnAtFix.ts rename to browser/src/parser/pathGenerators/handleTurnAtFix.ts diff --git a/browser/src/parser/terminators/AF.ts b/browser/src/parser/terminators/AF.ts index 94d72fb..0873a7e 100644 --- a/browser/src/parser/terminators/AF.ts +++ b/browser/src/parser/terminators/AF.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/CF.ts b/browser/src/parser/terminators/CF.ts new file mode 100644 index 0000000..47c23df --- /dev/null +++ b/browser/src/parser/terminators/CF.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/CI.ts b/browser/src/parser/terminators/CI.ts new file mode 100644 index 0000000..4c9b1f0 --- /dev/null +++ b/browser/src/parser/terminators/CI.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/CR.ts b/browser/src/parser/terminators/CR.ts new file mode 100644 index 0000000..41ba859 --- /dev/null +++ b/browser/src/parser/terminators/CR.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/FM.ts b/browser/src/parser/terminators/FM.ts new file mode 100644 index 0000000..b43b8ff --- /dev/null +++ b/browser/src/parser/terminators/FM.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/RF.ts b/browser/src/parser/terminators/RF.ts new file mode 100644 index 0000000..87132c4 --- /dev/null +++ b/browser/src/parser/terminators/RF.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/TF.ts b/browser/src/parser/terminators/TF.ts new file mode 100644 index 0000000..4c8c6f0 --- /dev/null +++ b/browser/src/parser/terminators/TF.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/VA.ts b/browser/src/parser/terminators/VA.ts new file mode 100644 index 0000000..941ce43 --- /dev/null +++ b/browser/src/parser/terminators/VA.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/VD.ts b/browser/src/parser/terminators/VD.ts new file mode 100644 index 0000000..6eeaa27 --- /dev/null +++ b/browser/src/parser/terminators/VD.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/VI.ts b/browser/src/parser/terminators/VI.ts new file mode 100644 index 0000000..8e534ef --- /dev/null +++ b/browser/src/parser/terminators/VI.ts @@ -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]; +}; diff --git a/browser/src/parser/terminators/VM.ts b/browser/src/parser/terminators/VM.ts new file mode 100644 index 0000000..def8fc0 --- /dev/null +++ b/browser/src/parser/terminators/VM.ts @@ -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]; +}; diff --git a/browser/src/parser/utils/computeIntersection.ts b/browser/src/parser/utils/computeIntersection.ts index a665e78..4f5b243 100644 --- a/browser/src/parser/utils/computeIntersection.ts +++ b/browser/src/parser/utils/computeIntersection.ts @@ -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); diff --git a/browser/src/parser/utils/extensions.ts b/browser/src/parser/utils/extensions.ts index 215ef69..e3f843d 100644 --- a/browser/src/parser/utils/extensions.ts +++ b/browser/src/parser/utils/extensions.ts @@ -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)); +}; diff --git a/browser/src/parser/utils/getCourseAndFixForIntercepts.ts b/browser/src/parser/utils/getCourseAndFixForIntercepts.ts new file mode 100644 index 0000000..9ee1004 --- /dev/null +++ b/browser/src/parser/utils/getCourseAndFixForIntercepts.ts @@ -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]; + } + } +}; diff --git a/browser/src/types/extensions.d.ts b/browser/src/types/extensions.d.ts index f5fe140..10476b9 100644 --- a/browser/src/types/extensions.d.ts +++ b/browser/src/types/extensions.d.ts @@ -26,4 +26,11 @@ export declare global { */ toMetre: () => number; } + + interface String { + /** + * @returns Value parsed from altitude constraint + */ + parseAltitude: () => number; + } } diff --git a/browser/src/types/terminators/AF.d.ts b/browser/src/types/terminators/AF.d.ts index 22c3f51..8f78119 100644 --- a/browser/src/types/terminators/AF.d.ts +++ b/browser/src/types/terminators/AF.d.ts @@ -2,10 +2,10 @@ export declare global { type AFTerminalEntry = Required< Pick< TerminalEntry, - | "TurnDir" | "WptID" | "WptLat" | "WptLon" + | "TurnDir" | "NavID" | "NavLat" | "NavLon" diff --git a/browser/src/types/terminators/CI.d.ts b/browser/src/types/terminators/CI.d.ts new file mode 100644 index 0000000..510a743 --- /dev/null +++ b/browser/src/types/terminators/CI.d.ts @@ -0,0 +1,4 @@ +export declare global { + type CITerminalEntry = Required> & + TerminalEntry; +} diff --git a/browser/src/types/terminators/CR.d.ts b/browser/src/types/terminators/CR.d.ts new file mode 100644 index 0000000..e2246d1 --- /dev/null +++ b/browser/src/types/terminators/CR.d.ts @@ -0,0 +1,6 @@ +export declare global { + type CRTerminalEntry = Required< + Pick + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/FM.d.ts b/browser/src/types/terminators/FM.d.ts new file mode 100644 index 0000000..91969ff --- /dev/null +++ b/browser/src/types/terminators/FM.d.ts @@ -0,0 +1,17 @@ +export declare global { + type FMTerminalEntry = Required< + Pick< + TerminalEntry, + | "WptID" + | "WptLat" + | "WptLon" + | "NavID" + | "NavLat" + | "NavLon" + | "NavBear" + | "NavDist" + | "Course" + > + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/RF.d.ts b/browser/src/types/terminators/RF.d.ts new file mode 100644 index 0000000..dde74a5 --- /dev/null +++ b/browser/src/types/terminators/RF.d.ts @@ -0,0 +1,18 @@ +export declare global { + type RFTerminalEntry = Required< + Pick< + TerminalEntry, + | "WptID" + | "WptLat" + | "WptLon" + | "TurnDir" + | "NavBear" + | "Course" + | "Distance" + | "CenterID" + | "CenterLat" + | "CenterLon" + > + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/TF.d.ts b/browser/src/types/terminators/TF.d.ts new file mode 100644 index 0000000..710e3be --- /dev/null +++ b/browser/src/types/terminators/TF.d.ts @@ -0,0 +1,6 @@ +export declare global { + type TFTerminalEntry = Required< + Pick + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/VA.d.ts b/browser/src/types/terminators/VA.d.ts new file mode 100644 index 0000000..2c512f9 --- /dev/null +++ b/browser/src/types/terminators/VA.d.ts @@ -0,0 +1,4 @@ +export declare global { + type VATerminalEntry = Required> & + TerminalEntry; +} diff --git a/browser/src/types/terminators/VD.d.ts b/browser/src/types/terminators/VD.d.ts new file mode 100644 index 0000000..7304543 --- /dev/null +++ b/browser/src/types/terminators/VD.d.ts @@ -0,0 +1,6 @@ +export declare global { + type VDTerminalEntry = Required< + Pick + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/VI.d.ts b/browser/src/types/terminators/VI.d.ts new file mode 100644 index 0000000..5827ef5 --- /dev/null +++ b/browser/src/types/terminators/VI.d.ts @@ -0,0 +1,4 @@ +export declare global { + type VITerminalEntry = Required> & + TerminalEntry; +} diff --git a/browser/src/types/terminators/VM.d.ts b/browser/src/types/terminators/VM.d.ts new file mode 100644 index 0000000..701b793 --- /dev/null +++ b/browser/src/types/terminators/VM.d.ts @@ -0,0 +1,4 @@ +export declare global { + type VMTerminalEntry = Required> & + TerminalEntry; +} diff --git a/browser/src/types/types.d.ts b/browser/src/types/types.d.ts index 18eb493..05ecb58 100644 --- a/browser/src/types/types.d.ts +++ b/browser/src/types/types.d.ts @@ -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]; diff --git a/geojson.js b/geojson.js deleted file mode 100644 index f855bde..0000000 --- a/geojson.js +++ /dev/null @@ -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)); diff --git a/package.json b/package.json deleted file mode 100644 index 2f76404..0000000 --- a/package.json +++ /dev/null @@ -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" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 4509d50..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -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: {}