diff --git a/README.md b/README.md index ca8a312..9d29937 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,9 @@ LGAV KOR1D SID (Cycle 2507, ID 10679) ### Instructions -- From present position, fly direct to fix identified by `WptID` +- From present position, fly direct to fix identified by `WptID`. +- If preceding fix and fix identified by `WptId` are the same, execute 360° turn + (requires `TurnDir`). ### Units diff --git a/browser/src/App.tsx b/browser/src/App.tsx index 2280d23..44b575e 100644 --- a/browser/src/App.tsx +++ b/browser/src/App.tsx @@ -9,19 +9,20 @@ import L from "leaflet"; const parser = await Parser.instance(); const terminals = [ - 10394, 10395, 10475, 10480, 10482, 10485, 10653, 10654, 10657, 10659, + 10394, 10395, 10475, 10480, 10482, 10485, 10653, 10654, 10657, 10659, 10679, + 11798, 11909, 12765, ]; function App() { const [selectedTerminal, setSelectedTerminal] = useState(terminals[0]); - const [procedure, setProcedure] = useState(); + const [procedures, setProcedures] = useState([]); const mapRef = createRef(); const layerRef = createRef(); useEffect(() => { (async () => { - setProcedure(await parser.parse(selectedTerminal)); + setProcedures(await parser.parse(selectedTerminal)); })(); }, [selectedTerminal]); @@ -49,87 +50,102 @@ function App() { attribution='© OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - ({ - color: "#ff00ff", - stroke: true, - weight: 5, - opacity: 1, - dashArray: properties.isManual ? "20, 20" : undefined, - })} - filter={(feature) => feature.geometry.type !== "Point"} - ref={layerRef} - /> - { - if (properties.isFlyOver) - return L.shapeMarker(latlng, { - shape: "triangle", - radius: 6, - }); - if (properties.isIntersection) - return L.circleMarker(latlng, { radius: 6 }); + {procedures.map((procedure) => ( + <> + ({ + color: "#ff00ff", + stroke: true, + weight: 5, + opacity: 1, + dashArray: properties.isManual ? "20, 20" : undefined, + })} + filter={(feature) => feature.geometry.type !== "Point"} + ref={layerRef} + /> + { + if (properties.isFlyOver) + return L.shapeMarker(latlng, { + shape: "triangle", + radius: 6, + }); + if (properties.isIntersection) + return L.circleMarker(latlng, { radius: 6 }); - return L.shapeMarker(latlng, { - shape: "star-4", - radius: 10, - rotation: 45, - }); - }} - onEachFeature={({ geometry, properties }, layer) => { - if (geometry.type === "Point") { - layer.bindPopup( - `${properties.name}
+ return L.shapeMarker(latlng, { + shape: "star-4", + radius: 10, + rotation: 45, + }); + }} + 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"} - /> + ); + } + }} + filter={(feature) => feature.geometry.type === "Point"} + /> + + ))}
- {terminals.map((terminal) => ( -
setSelectedTerminal(terminal)} - > - - {(() => { - const t = parser.terminals.find(({ ID }) => ID === terminal); - return `${t?.ICAO} - ${t?.FullName}`; - })()} - -
({terminal})
-
- ))} +
+ {terminals.map((terminal) => ( +
setSelectedTerminal(terminal)} + > + + {(() => { + const t = parser.terminals.find(({ ID }) => ID === terminal); + return `${t?.ICAO} - ${t?.FullName}`; + })()} + +
({terminal})
+
+ ))} +
); diff --git a/browser/src/parser/node.ts b/browser/src/parser/node.ts index 7248e6a..f495111 100644 --- a/browser/src/parser/node.ts +++ b/browser/src/parser/node.ts @@ -14,4 +14,4 @@ fetch = async (path: string) => { const parser = await Parser.instance(); -console.log(JSON.stringify(await parser.parse(10480))); +console.log(JSON.stringify(await parser.parse(12765))); diff --git a/browser/src/parser/parser.ts b/browser/src/parser/parser.ts index 6ce96c1..eeadbdb 100644 --- a/browser/src/parser/parser.ts +++ b/browser/src/parser/parser.ts @@ -17,6 +17,15 @@ import { TerminatorsDF } from "./terminators/DF.ts"; import { TerminatorsFD } from "./terminators/FD.ts"; import { TerminatorsFA } from "./terminators/FA.ts"; import { TerminatorsCD } from "./terminators/CD.ts"; +import { TerminatorsVR } from "./terminators/VR.ts"; +import { TerminatorsIF } from "./terminators/IF.ts"; +import { TerminatorsFC } from "./terminators/FC.ts"; + +/* +Runway IDs for LIED +26156 - 34L +26157 - 34R +*/ class Parser { private static _instance: Parser; @@ -62,244 +71,286 @@ class Parser { } public parse = async (terminalID: number) => { - // Private functions - /** - * @param line Line segments - */ - const updateLastCourse = (line: LineSegment[]) => { - lastCourse = geolib.getGreatCircleBearing( - { - latitude: line.at(-2)![1], - longitude: line.at(-2)![0], - }, - { - latitude: line.at(-1)![1], - longitude: line.at(-1)![0], - } - ); - }; - - /** - * @param fix New fix - * @param line New line - * @param options Options for line rendering - */ - const update = ( - fix?: NavFix, - line?: LineSegment[], - options?: Record - ) => { - if (fix) navFixes.push(fix); - if (line) { - lineSegments.push({ line, ...options }); - updateLastCourse(line); - } - }; - // Get Procedure main const terminal = this.terminals.find(({ ID }) => ID === terminalID); if (!terminal) throw new Error("Procedure does not exists"); // Get runway this procedure is for - const runway = this.runways.find(({ ID }) => ID === terminal.RwyID); - if (!runway) throw new Error("Procedure links to non existent Runway"); + let runway = this.runways.find(({ ID }) => ID === terminal.RwyID); + if (!runway) { + let id = 26156; + if (typeof prompt !== "undefined") + // throw new Error("Prompt not defined, cannot continue"); + + id = Number.parseInt(prompt("Runway ID") ?? ""); + runway = this.runways.find(({ ID }) => ID === id); + if (!runway) throw new Error("Procedure links to non existent Runway"); + } // Load procedure - const procedure = (await ( + const procedures = (await ( await fetch(`NavData/TermID_${terminalID}.json`) ).json()) as TerminalEntry[]; + // Split into transitions + const transitions = new Set(procedures.map((proc) => proc.Transition)); - // Output variables - const navFixes: NavFix[] = []; - const lineSegments: { line: LineSegment[]; [x: string]: unknown }[] = []; - - // Initials - navFixes.push({ - latitude: runway.Latitude, - longitude: runway.Longitude, - altitude: runway.Elevation, - speed: 0, - name: runway.Ident, - }); - let lastCourse = runway.TrueHeading; + const output: object[] = []; // Main - for (let index = 0; index < procedure.length; index++) { - const leg = procedure[index]; - const previousFix = navFixes.at(-1)!; - const waypoint = this.waypoints.filter(({ ID }) => ID === leg.WptID)[0]; + transitions.forEach((transition) => { + // Private functions + /** + * @param line Line segments + */ + const updateLastCourse = (line: LineSegment[]) => { + lastCourse = geolib.getGreatCircleBearing( + { + latitude: line.at(-2)![1], + longitude: line.at(-2)![0], + }, + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + } + ); + }; - switch (leg.TrackCode) { - case "AF": { - const [fixToAdd, lineToAdd] = TerminatorsAF( - leg as AFTerminalEntry, - previousFix, - waypoint - ); - update(fixToAdd, lineToAdd); - break; + /** + * @param fix New fix + * @param line New line + * @param options Options for line rendering + */ + const update = ( + fix?: NavFix, + line?: LineSegment[], + options?: Record + ) => { + if (fix) navFixes.push(fix); + if (line) { + lineSegments.push({ line, ...options }); + updateLastCourse(line); } - case "CA": { - const [fixToAdd, lineToAdd] = TerminatorsCA( - leg as CATerminalEntry, - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd); - break; + }; + + // Output variables + const navFixes: NavFix[] = []; + const lineSegments: { line: LineSegment[]; [x: string]: unknown }[] = []; + + // Initials + navFixes.push({ + latitude: runway.Latitude, + longitude: runway.Longitude, + altitude: runway.Elevation, + speed: 0, + name: runway.Ident, + }); + let lastCourse = runway.TrueHeading; + + const procedure = procedures.filter( + (proc) => proc.Transition === transition + ); + for (let index = 0; index < procedure.length; index++) { + const leg = procedure[index]; + const previousFix = navFixes.at(-1)!; + const waypoint = this.waypoints.filter(({ ID }) => ID === leg.WptID)[0]; + + switch (leg.TrackCode) { + case "AF": { + const [fixToAdd, lineToAdd] = TerminatorsAF( + leg as AFTerminalEntry, + previousFix, + waypoint + ); + update(fixToAdd, lineToAdd); + break; + } + case "CA": { + const [fixToAdd, lineToAdd] = TerminatorsCA( + leg as CATerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "CD": { + const [fixToAdd, lineToAdd] = TerminatorsCD( + leg as CDTerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "CF": { + const [fixToAdd, lineToAdd] = TerminatorsCF( + leg as CFTerminalEntry, + previousFix, + lastCourse, + waypoint + ); + update(fixToAdd, lineToAdd); + break; + } + case "CI": { + const [fixToAdd, lineToAdd] = TerminatorsCI( + leg as CITerminalEntry, + procedure[index + 1], + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "CR": { + const [fixToAdd, lineToAdd] = TerminatorsCR( + leg as CRTerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "DF": { + const [fixToAdd, lineToAdd] = TerminatorsDF( + leg as DFTerminalEntry, + previousFix, + lastCourse, + waypoint + ); + update(fixToAdd, lineToAdd); + break; + } + case "FA": { + const [fixToAdd, lineToAdd] = TerminatorsFA( + leg as FATerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "FC": { + const [fixToAdd, lineToAdd] = TerminatorsFC( + leg as FCTerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "FD": { + const [fixToAdd, lineToAdd] = TerminatorsFD( + leg as FDTerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "FM": { + const [fixToAdd, lineToAdd] = TerminatorsFM( + leg as FMTerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd, { isManual: true }); + break; + } + case "HA": + case "HF": + case "HM": + console.error("Unknown TrackCode", leg.TrackCode); + break; + case "IF": { + const fixToAdd = TerminatorsIF(leg as RFTerminalEntry, waypoint); + navFixes.length = 0; + navFixes.push(fixToAdd); + break; + } + case "PI": + console.error("Unknown TrackCode", leg.TrackCode); + break; + case "RF": { + const [fixToAdd, lineToAdd] = TerminatorsRF( + leg as RFTerminalEntry, + procedure[index + 1], + previousFix, + lastCourse, + waypoint + ); + update(fixToAdd, lineToAdd); + break; + } + case "TF": { + const [fixToAdd, lineToAdd] = TerminatorsTF( + leg as TFTerminalEntry, + previousFix, + lastCourse, + waypoint + ); + update(fixToAdd, lineToAdd); + break; + } + case "VA": { + const [fixToAdd, lineToAdd] = TerminatorsVA( + leg as VATerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "VD": { + const [fixToAdd, lineToAdd] = TerminatorsVD( + leg as VDTerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "VI": { + const [fixToAdd, lineToAdd] = TerminatorsVI( + leg as VITerminalEntry, + procedure[index + 1], + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + case "VM": { + const [fixToAdd, lineToAdd] = TerminatorsVM( + leg as VMTerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd, { isManual: true }); + break; + } + case "VR": { + const [fixToAdd, lineToAdd] = TerminatorsVR( + leg as VRTerminalEntry, + previousFix, + lastCourse + ); + update(fixToAdd, lineToAdd); + break; + } + default: + console.error("Unknown TrackCode", leg.TrackCode); + break; } - case "CD": { - const [fixToAdd, lineToAdd] = TerminatorsCD( - leg as CDTerminalEntry, - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd); - break; - } - case "CF": { - const [fixToAdd, lineToAdd] = TerminatorsCF( - leg as CFTerminalEntry, - previousFix, - lastCourse, - waypoint - ); - update(fixToAdd, lineToAdd); - break; - } - case "CI": { - const [fixToAdd, lineToAdd] = TerminatorsCI( - leg as CITerminalEntry, - procedure[index + 1], - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd); - break; - } - case "CR": { - const [fixToAdd, lineToAdd] = TerminatorsCR( - leg as CRTerminalEntry, - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd); - break; - } - case "DF": { - const [fixToAdd, lineToAdd] = TerminatorsDF( - leg as DFTerminalEntry, - previousFix, - lastCourse, - waypoint - ); - update(fixToAdd, lineToAdd); - break; - } - case "FA": { - const [fixToAdd, lineToAdd] = TerminatorsFA( - leg as FATerminalEntry, - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd); - break; - } - case "FC": - console.error("Unknown TrackCode", leg.TrackCode); - break; - case "FD": { - const [fixToAdd, lineToAdd] = TerminatorsFD( - leg as FDTerminalEntry, - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd); - break; - } - case "FM": { - const [fixToAdd, lineToAdd] = TerminatorsFM( - leg as FMTerminalEntry, - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd, { isManual: true }); - break; - } - case "HA": - case "HF": - case "HM": - case "IF": - case "PI": - console.error("Unknown TrackCode", leg.TrackCode); - break; - case "RF": { - const [fixToAdd, lineToAdd] = TerminatorsRF( - leg as RFTerminalEntry, - procedure[index + 1], - previousFix, - lastCourse, - waypoint - ); - update(fixToAdd, lineToAdd); - break; - } - case "TF": { - const [fixToAdd, lineToAdd] = TerminatorsTF( - leg as TFTerminalEntry, - previousFix, - lastCourse, - waypoint - ); - update(fixToAdd, lineToAdd); - break; - } - case "VA": { - const [fixToAdd, lineToAdd] = TerminatorsVA( - leg as VATerminalEntry, - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd); - break; - } - case "VD": { - const [fixToAdd, lineToAdd] = TerminatorsVD( - leg as VDTerminalEntry, - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd); - break; - } - case "VI": { - const [fixToAdd, lineToAdd] = TerminatorsVI( - leg as VITerminalEntry, - procedure[index + 1], - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd); - break; - } - case "VM": { - const [fixToAdd, lineToAdd] = TerminatorsVM( - leg as VMTerminalEntry, - previousFix, - lastCourse - ); - update(fixToAdd, lineToAdd, { isManual: true }); - break; - } - case "VR": - default: - console.error("Unknown TrackCode", leg.TrackCode); - break; } - } - return geojson.parse([...navFixes, ...lineSegments], { - LineString: "line", - Point: ["latitude", "longitude"], + output.push( + geojson.parse([...navFixes, ...lineSegments], { + LineString: "line", + Point: ["latitude", "longitude"], + }) + ); }); + + return output; }; } diff --git a/browser/src/parser/pathGenerators/generatePerformanceArc.ts b/browser/src/parser/pathGenerators/generatePerformanceArc.ts index a839c07..2870fc1 100644 --- a/browser/src/parser/pathGenerators/generatePerformanceArc.ts +++ b/browser/src/parser/pathGenerators/generatePerformanceArc.ts @@ -8,6 +8,7 @@ import { computeTurnRate } from "../utils/computeTurnRate.ts"; * @param start Arc origin point * @param speed Speed within arc * @param turnDir Turn direction + * @param force360 If true, force a 360° turn * @returns Line segments */ export const generatePerformanceArc = ( @@ -15,14 +16,16 @@ export const generatePerformanceArc = ( crsFromOrigin: number, start: NavFix, speed: number, - turnDir?: TurnDirection + turnDir?: TurnDirection, + force360?: boolean ) => { const turnRate = computeTurnRate(speed, Parser.AC_BANK); + const originalCrsFromOrigin = crsFromOrigin; const line: LineSegment[] = [[start.longitude, start.latitude]]; // Check if there even is an arc - if (!crsFromOrigin.equal(crsIntoEndpoint)) { + if (force360 || !crsFromOrigin.equal(crsIntoEndpoint)) { // Turn Dir if (!turnDir || turnDir === "E") { let prov = crsFromOrigin - crsIntoEndpoint; @@ -56,6 +59,39 @@ export const generatePerformanceArc = ( line.push([arcFix.longitude, arcFix.latitude]); } + + // Second half + if (force360) { + const temp = crsIntoEndpoint; + crsIntoEndpoint = originalCrsFromOrigin; + crsFromOrigin = temp; + + while (!crsFromOrigin.equal(crsIntoEndpoint)) { + let time = 0; + if (turnDir === "R") { + const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees(); + const increment = delta < 1 ? delta : 1; + crsFromOrigin = (crsFromOrigin + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees(); + const increment = delta < 1 ? delta : 1; + crsFromOrigin = (crsFromOrigin - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = geolib.computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed / 3600) * time).toMetre(), + crsFromOrigin + ); + + line.push([arcFix.longitude, arcFix.latitude]); + } + } } return line; diff --git a/browser/src/parser/pathGenerators/generateTangentArc.ts b/browser/src/parser/pathGenerators/generateTangentArc.ts index a6d73e7..d5b5568 100644 --- a/browser/src/parser/pathGenerators/generateTangentArc.ts +++ b/browser/src/parser/pathGenerators/generateTangentArc.ts @@ -89,7 +89,6 @@ export const generateTangentArc = ( crsOrthogonalOnOrigin -= delta < 1 ? delta : 1; crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); } - if (crsOrthogonalOnOrigin === crsOrthogonalOnEndpoint) break; const arcFix = geolib.computeDestinationPoint( arcCenter, diff --git a/browser/src/parser/terminators/AF.ts b/browser/src/parser/terminators/AF.ts index dc78372..2ef3159 100644 --- a/browser/src/parser/terminators/AF.ts +++ b/browser/src/parser/terminators/AF.ts @@ -28,6 +28,7 @@ export const TerminatorsAF = ( longitude: leg.WptLon, } ); + const line = generateAFArc( arcEndCrs, leg.Course.toTrue({ latitude: leg.NavLat, longitude: leg.NavLon }), diff --git a/browser/src/parser/terminators/CA.ts b/browser/src/parser/terminators/CA.ts index ce127dc..af9ffa3 100644 --- a/browser/src/parser/terminators/CA.ts +++ b/browser/src/parser/terminators/CA.ts @@ -12,6 +12,7 @@ export const TerminatorsCA = ( const crsIntoEndpoint = leg.Course.toTrue(previousFix); let line: LineSegment[] = []; + if (previousFix.isFlyOver) { line = generatePerformanceArc( crsIntoEndpoint, @@ -48,7 +49,7 @@ export const TerminatorsCA = ( name: leg.Alt, isFlyOver: true, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; diff --git a/browser/src/parser/terminators/CD.ts b/browser/src/parser/terminators/CD.ts index 259d3b1..26a460c 100644 --- a/browser/src/parser/terminators/CD.ts +++ b/browser/src/parser/terminators/CD.ts @@ -15,6 +15,7 @@ export const TerminatorsCD = ( const speed = computeSpeed(leg, previousFix); let line: LineSegment[] = []; + if (previousFix.isFlyOver) { const crsIntoEndpoint = leg.Course.toTrue(previousFix); @@ -58,7 +59,7 @@ export const TerminatorsCD = ( name: leg.Distance.toString(), isFlyOver: true, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; diff --git a/browser/src/parser/terminators/CF.ts b/browser/src/parser/terminators/CF.ts index cf4e776..adf64a5 100644 --- a/browser/src/parser/terminators/CF.ts +++ b/browser/src/parser/terminators/CF.ts @@ -15,7 +15,7 @@ export const TerminatorsCF = ( name: waypoint?.Ident ?? undefined, isFlyOver: leg.IsFlyOver, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; diff --git a/browser/src/parser/terminators/CI.ts b/browser/src/parser/terminators/CI.ts index 5a9e4d0..6196df7 100644 --- a/browser/src/parser/terminators/CI.ts +++ b/browser/src/parser/terminators/CI.ts @@ -22,7 +22,7 @@ export const TerminatorsCI = ( )!, isFlyOver: leg.IsFlyOver, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; diff --git a/browser/src/parser/terminators/CR.ts b/browser/src/parser/terminators/CR.ts index 7d4b8de..9dd6e7d 100644 --- a/browser/src/parser/terminators/CR.ts +++ b/browser/src/parser/terminators/CR.ts @@ -1,38 +1,56 @@ +import * as geolib from "geolib"; import { computeIntersection } from "../utils/computeIntersection.ts"; -import { handleTurnAtFix } from "../pathGenerators/handleTurnAtFix.ts"; import { computeSpeed } from "../utils/computeSpeed.ts"; +import { generatePerformanceArc } from "../pathGenerators/generatePerformanceArc.ts"; export const TerminatorsCR = ( leg: CRTerminalEntry, previousFix: NavFix, lastCourse: number ): [NavFix?, LineSegment[]?] => { - const crsIntoEndpoint = leg.Course.toTrue(previousFix); + const navaid = { + latitude: leg.NavLat, + longitude: leg.NavLon, + }; + const crsFromEndpoint = leg.Course.toTrue(previousFix); + const crsIntoEndpoint = leg.NavBear.toTrue(navaid); const speed = computeSpeed(leg, previousFix); - const interceptFix: NavFix = { - ...computeIntersection( + let line: LineSegment[] = []; + + if (previousFix.isFlyOver) { + line = generatePerformanceArc( + crsFromEndpoint, + lastCourse, previousFix, - crsIntoEndpoint, - { latitude: leg.NavLat, longitude: leg.NavLon }, - leg.NavBear.toTrue({ latitude: leg.NavLat, longitude: leg.NavLon }) - )!, + speed, + leg.TurnDir + ); + } else { + line.push([previousFix.longitude, previousFix.latitude]); + } + + const arcEnd = { latitude: line.at(-1)![1], longitude: line.at(-1)![0] }; + if (line.length > 1) { + lastCourse = geolib.getGreatCircleBearing( + { + latitude: line.at(-2)![1], + longitude: line.at(-2)![0], + }, + arcEnd + ); + } + + const interceptFix: NavFix = { + ...computeIntersection(arcEnd, crsFromEndpoint, navaid, crsIntoEndpoint)!, isFlyOver: leg.IsFlyOver, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; - const line = handleTurnAtFix( - crsIntoEndpoint, - leg.Course.toTrue(previousFix), - lastCourse, - previousFix, - interceptFix, - speed, - leg.TurnDir - ); + line.push([interceptFix.longitude, interceptFix.latitude]); return [interceptFix, line]; }; diff --git a/browser/src/parser/terminators/DF.ts b/browser/src/parser/terminators/DF.ts index 326b289..c87dfa1 100644 --- a/browser/src/parser/terminators/DF.ts +++ b/browser/src/parser/terminators/DF.ts @@ -22,6 +22,7 @@ export const TerminatorsDF = ( }; let line: LineSegment[] = []; + if (previousFix.isFlyOver) { const crsIntoEndpoint = geolib.getGreatCircleBearing( previousFix, @@ -33,7 +34,9 @@ export const TerminatorsDF = ( lastCourse, previousFix, speed, - leg.TurnDir + leg.TurnDir, + previousFix.latitude.equal(targetFix.latitude) && + previousFix.longitude.equal(targetFix.longitude) ); } else { line.push([previousFix.longitude, previousFix.latitude]); diff --git a/browser/src/parser/terminators/FA.ts b/browser/src/parser/terminators/FA.ts index 7497591..9ef3123 100644 --- a/browser/src/parser/terminators/FA.ts +++ b/browser/src/parser/terminators/FA.ts @@ -16,6 +16,7 @@ export const TerminatorsFA = ( const crsIntoEndpoint = leg.Course.toTrue(refFix); let line: LineSegment[] = []; + if (previousFix.isFlyOver) { line = generatePerformanceArc( crsIntoEndpoint, @@ -52,7 +53,7 @@ export const TerminatorsFA = ( name: leg.Alt, isFlyOver: true, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; diff --git a/browser/src/parser/terminators/FC.ts b/browser/src/parser/terminators/FC.ts new file mode 100644 index 0000000..545773f --- /dev/null +++ b/browser/src/parser/terminators/FC.ts @@ -0,0 +1,98 @@ +import * as geolib from "geolib"; +import { computeSpeed } from "../utils/computeSpeed.ts"; +import Parser from "../parser.ts"; +import { computeTurnRate } from "../utils/computeTurnRate.ts"; + +// NOTE: Distance not adjusted for altitude in this demo +export const TerminatorsFC = ( + leg: FCTerminalEntry, + previousFix: NavFix, + lastCourse: number +): [NavFix?, LineSegment[]?] => { + const refFix = { + latitude: leg.WptLat, + longitude: leg.WptLon, + }; + const speed = computeSpeed(leg, previousFix); + const turnRate = computeTurnRate(speed, Parser.AC_BANK); + const trackIntoEndpoint = leg.Course.toTrue(refFix); + + const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; + + if (previousFix.isFlyOver) { + let crsIntoEndpoint = trackIntoEndpoint; + + // Check if there even is an arc + if (!crsIntoEndpoint.equal(lastCourse)) { + // Turn Dir + if (!leg.TurnDir || leg.TurnDir === "E") { + let prov = lastCourse - crsIntoEndpoint; + prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov; + leg.TurnDir = prov > 0 ? "L" : "R"; + } + + // Generate arc + let condition = false; + do { + let time = 0; + if (leg.TurnDir === "R") { + const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); + const increment = delta < 1 ? delta : 1; + lastCourse = (lastCourse + increment).normaliseDegrees(); + time = increment / turnRate; + } else { + const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); + const increment = delta < 1 ? delta : 1; + lastCourse = (lastCourse - increment).normaliseDegrees(); + time = increment / turnRate; + } + + const arcFix = geolib.computeDestinationPoint( + { + latitude: line.at(-1)![1], + longitude: line.at(-1)![0], + }, + ((speed * time) / 3600).toMetre(), + lastCourse + ); + + line.push([arcFix.longitude, arcFix.latitude]); + + if (leg.TurnDir === "R") { + condition = lastCourse > trackIntoEndpoint; + } else { + condition = lastCourse < trackIntoEndpoint; + } + } while (condition); + } + } + + const arcEnd = { latitude: line.at(-1)![1], longitude: line.at(-1)![0] }; + if (line.length > 1) { + lastCourse = geolib.getGreatCircleBearing( + { + latitude: line.at(-2)![1], + longitude: line.at(-2)![0], + }, + arcEnd + ); + } + + const targetFix: NavFix = { + ...geolib.computeDestinationPoint( + arcEnd, + leg.Distance.toMetre(), + lastCourse + ), + name: leg.Distance.toString(), + isFlyOver: true, + altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, + speed: speed, + speedConstraint: leg.SpeedLimit, + altitudeConstraint: leg.Alt, + }; + + line.push([targetFix.longitude, targetFix.latitude]); + + return [targetFix, line]; +}; diff --git a/browser/src/parser/terminators/FD.ts b/browser/src/parser/terminators/FD.ts index 4d686ca..d3648be 100644 --- a/browser/src/parser/terminators/FD.ts +++ b/browser/src/parser/terminators/FD.ts @@ -19,6 +19,7 @@ export const TerminatorsFD = ( const speed = computeSpeed(leg, previousFix); let line: LineSegment[] = []; + if (previousFix.isFlyOver) { const crsIntoEndpoint = leg.Course.toTrue(refFix); @@ -62,7 +63,7 @@ export const TerminatorsFD = ( name: leg.Distance.toString(), isFlyOver: true, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; diff --git a/browser/src/parser/terminators/IF.ts b/browser/src/parser/terminators/IF.ts new file mode 100644 index 0000000..2e74d64 --- /dev/null +++ b/browser/src/parser/terminators/IF.ts @@ -0,0 +1,19 @@ +import Parser from "../parser.ts"; + +export const TerminatorsIF = ( + leg: IFTerminalEntry, + waypoint?: Waypoint +): NavFix => { + const targetFix: NavFix = { + latitude: leg.WptLat, + longitude: leg.WptLon, + name: waypoint?.Ident ?? undefined, + isFlyOver: leg.IsFlyOver, + altitude: leg.Alt ? leg.Alt.parseAltitude() : 0, + speed: leg.SpeedLimit ? leg.SpeedLimit : Parser.AC_SPEED, + speedConstraint: leg.SpeedLimit, + altitudeConstraint: leg.Alt, + }; + + return targetFix; +}; diff --git a/browser/src/parser/terminators/RF.ts b/browser/src/parser/terminators/RF.ts index ae968da..a423811 100644 --- a/browser/src/parser/terminators/RF.ts +++ b/browser/src/parser/terminators/RF.ts @@ -29,6 +29,7 @@ export const TerminatorsRF = ( { latitude: leg.CenterLat, longitude: leg.CenterLon }, leg.TurnDir ); + line.push([targetFix.longitude, targetFix.latitude]); return [targetFix, line]; diff --git a/browser/src/parser/terminators/VA.ts b/browser/src/parser/terminators/VA.ts index 32f0e73..f054e6e 100644 --- a/browser/src/parser/terminators/VA.ts +++ b/browser/src/parser/terminators/VA.ts @@ -13,6 +13,7 @@ export const TerminatorsVA = ( const crsIntoEndpoint = leg.Course.toTrue(previousFix); let line: LineSegment[] = []; + if (previousFix.isFlyOver) { line = generatePerformanceArc( crsIntoEndpoint, @@ -49,7 +50,7 @@ export const TerminatorsVA = ( name: leg.Alt, isFlyOver: true, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; diff --git a/browser/src/parser/terminators/VD.ts b/browser/src/parser/terminators/VD.ts index f8c8afc..a104df3 100644 --- a/browser/src/parser/terminators/VD.ts +++ b/browser/src/parser/terminators/VD.ts @@ -16,6 +16,7 @@ export const TerminatorsVD = ( const speed = computeSpeed(leg, previousFix); let line: LineSegment[] = []; + if (previousFix.isFlyOver) { const crsIntoEndpoint = leg.Course.toTrue(previousFix); @@ -59,7 +60,7 @@ export const TerminatorsVD = ( name: leg.Distance.toString(), isFlyOver: true, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; diff --git a/browser/src/parser/terminators/VI.ts b/browser/src/parser/terminators/VI.ts index 141865f..f33c0fc 100644 --- a/browser/src/parser/terminators/VI.ts +++ b/browser/src/parser/terminators/VI.ts @@ -23,7 +23,7 @@ export const TerminatorsVI = ( )!, isFlyOver: leg.IsFlyOver, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, - speed: computeSpeed(leg, previousFix), + speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, }; diff --git a/browser/src/parser/terminators/VR.ts b/browser/src/parser/terminators/VR.ts new file mode 100644 index 0000000..84adb2d --- /dev/null +++ b/browser/src/parser/terminators/VR.ts @@ -0,0 +1,57 @@ +import * as geolib from "geolib"; +import { computeIntersection } from "../utils/computeIntersection.ts"; +import { computeSpeed } from "../utils/computeSpeed.ts"; +import { generatePerformanceArc } from "../pathGenerators/generatePerformanceArc.ts"; + +// NOTE: No wind adjustments to be made, no clue how *that* would draw +export const TerminatorsVR = ( + leg: VRTerminalEntry, + previousFix: NavFix, + lastCourse: number +): [NavFix?, LineSegment[]?] => { + const navaid = { + latitude: leg.NavLat, + longitude: leg.NavLon, + }; + const crsFromEndpoint = leg.Course.toTrue(previousFix); + const crsIntoEndpoint = leg.NavBear.toTrue(navaid); + const speed = computeSpeed(leg, previousFix); + + let line: LineSegment[] = []; + + if (previousFix.isFlyOver) { + line = generatePerformanceArc( + crsFromEndpoint, + lastCourse, + previousFix, + speed, + leg.TurnDir + ); + } else { + line.push([previousFix.longitude, previousFix.latitude]); + } + + const arcEnd = { latitude: line.at(-1)![1], longitude: line.at(-1)![0] }; + if (line.length > 1) { + lastCourse = geolib.getGreatCircleBearing( + { + latitude: line.at(-2)![1], + longitude: line.at(-2)![0], + }, + arcEnd + ); + } + + const interceptFix: NavFix = { + ...computeIntersection(arcEnd, crsFromEndpoint, navaid, crsIntoEndpoint)!, + isFlyOver: leg.IsFlyOver, + altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, + speed: speed, + speedConstraint: leg.SpeedLimit, + altitudeConstraint: leg.Alt, + }; + + line.push([interceptFix.longitude, interceptFix.latitude]); + + return [interceptFix, line]; +}; diff --git a/browser/src/types/terminators/FC.d.ts b/browser/src/types/terminators/FC.d.ts new file mode 100644 index 0000000..aa83e47 --- /dev/null +++ b/browser/src/types/terminators/FC.d.ts @@ -0,0 +1,19 @@ +export declare global { + type FCTerminalEntry = Required< + Pick< + TerminalEntry, + | "WptID" + | "WptLat" + | "WptLon" + | "IsFlyOver" + | "NavID" + | "NavLat" + | "NavLon" + | "NavBear" + | "NavDist" + | "Course" + | "Distance" + > + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/IF.d.ts b/browser/src/types/terminators/IF.d.ts new file mode 100644 index 0000000..e33a797 --- /dev/null +++ b/browser/src/types/terminators/IF.d.ts @@ -0,0 +1,6 @@ +export declare global { + type IFTerminalEntry = Required< + Pick + > & + TerminalEntry; +} diff --git a/browser/src/types/terminators/VR.d.ts b/browser/src/types/terminators/VR.d.ts new file mode 100644 index 0000000..cdcb36b --- /dev/null +++ b/browser/src/types/terminators/VR.d.ts @@ -0,0 +1,6 @@ +export declare global { + type VRTerminalEntry = Required< + Pick + > & + TerminalEntry; +}