Compare commits

..
2 Commits
Author SHA1 Message Date
Kilian 2bdbb583a8 Begin Refactor + Webapp 2025-07-13 22:53:20 +02:00
Kilian 2c99b701ce Types AF;RF 2025-07-13 22:51:43 +02:00
51 changed files with 3208 additions and 45 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Parser Node",
"skipFiles": ["<node_internals>/**"],
"args": [
"--experimental-strip-types",
"${workspaceFolder}\\browser\\src\\parser\\node.ts"
],
"cwd": "${workspaceFolder}\\browser\\",
"outFiles": ["${workspaceFolder}/**/*.js"]
}
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"cSpell.words": ["intc"]
}
+2 -2
View File
@@ -27,7 +27,8 @@ LGAV BIBE1K SID (Cycle 2507, ID 10653)
(`NavID`, `NavLat`, `NavLon`, `NavDist`, `NavBear`).
- Arc radius shall be `NavDist`.
- Arc center shall be navaid identified by `NavID`, `NavLat`, `NavLon`.
- `Course` shall be the boundary radial on which the arc begins (preceding fix lies hereupon).
- `Course` shall be the boundary radial on which the arc begins, measured from the navaid
identified by (`NavID`, `NavLat`, `NavLon`, `NavDist`, `NavBear`).
- Arc and turn shall be flown in direction specified by `TurnDir`.
### Units
@@ -512,7 +513,6 @@ LFRN GODA5R SID (cycle 2507, ID 10485)
### Instructions
- From preceding fix, fly a smooth arc to the fix identified by (`WptID`, `WptLat`, `WptLon`).
- Arc radius shall be `NavDist`.
- Arc center shall be navaid identified by `CenterID`, `CenterLat`, `CenterLon`.
- Arc and turn shall be flown in direction specified by `TurnDir`.
- `Distance` shall be the track miles along the curved path
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+69
View File
@@ -0,0 +1,69 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
+23
View File
@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MD-11 NavData Browser</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
{
"name": "browser",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"parser": "node --experimental-strip-types src/parser/node.ts"
},
"dependencies": {
"geojson": "^0.5.0",
"geolib": "^3.3.4",
"leaflet": "^1.9.4",
"magvar": "^2.0.0",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"react-leaflet": "5.0.0-rc.2"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/leaflet": "^1.9.20",
"@types/object-hash": "^3.0.6",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react-swc": "^3.10.2",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"object-hash": "^3.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4"
}
}
+1931
View File
File diff suppressed because it is too large. Load diff
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+45
View File
@@ -0,0 +1,45 @@
import { MapContainer, GeoJSON, TileLayer } from "react-leaflet";
import Parser from "./parser/parser";
import { useEffect, useState } from "react";
import hash from "object-hash";
const parser = await Parser.instance();
function App() {
const [procedure, setProcedure] = useState<string>();
console.log(procedure);
useEffect(() => {
(async () => {
setProcedure(await parser.parse(10394));
})();
}, []);
return (
<div style={{ display: "flex", height: "100vh", width: "100vw" }}>
<MapContainer
center={[51.505, -0.09]}
zoom={13}
style={{ height: "100%", width: "100%" }}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<GeoJSON
key={hash(procedure ?? "")}
data={procedure}
style={{
color: "#00ffff",
stroke: true,
weight: 2,
opacity: 1,
}}
/>
</MapContainer>
<div></div>
</div>
);
}
export default App;
+8
View File
@@ -0,0 +1,8 @@
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
}
+12
View File
@@ -0,0 +1,12 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import "leaflet/dist/leaflet.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);
+16
View File
@@ -0,0 +1,16 @@
import Parser from "./parser.ts";
// mutate fetch to be local
// @ts-ignore
fetch = async (path: string) => {
// @ts-ignore
const fs = await import("fs");
return {
json: () => JSON.parse(fs.readFileSync(`public/${path}`)),
};
};
const parser = await Parser.instance();
console.log(JSON.stringify(await parser.parse(10394)));
+148
View File
@@ -0,0 +1,148 @@
import "./utils/extensions.ts";
import { handleTurnAtFix } from "./utils/handleTurnAtFix.ts";
import * as geolib from "geolib";
import geojson from "geojson";
class Parser {
private static _instance: Parser;
private waypoints: Waypoint[];
private runways: Runway[];
private terminals: Terminal[];
public static AC_SPEED = 250;
private constructor(
waypoints: Waypoint[],
runways: Runway[],
terminals: Terminal[]
) {
this.waypoints = waypoints;
this.runways = runways;
this.terminals = terminals;
}
public static instance = async () => {
if (!Parser._instance) {
const waypoints = await (await fetch("navdata/Waypoints.json")).json();
const runways = await (await fetch("navdata/Runways.json")).json();
const terminals = await (await fetch("navdata/Terminals.json")).json();
Parser._instance = new Parser(waypoints, runways, terminals);
}
return Parser._instance;
};
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],
}
);
};
// 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");
// Load procedure
const procedure = (await (
await fetch(`navdata/TermID_${terminalID}.json`)
).json()) as TerminalEntry[];
// Output variables
const navFixes: NavFix[] = [];
const lineSegments: { line: LineSegment[] }[] = [];
// Initials
navFixes.push({
latitude: runway.Latitude,
longitude: runway.Longitude,
altitude: runway.Elevation,
speed: 0,
name: runway.Ident,
});
let lastCourse = runway.TrueHeading;
// 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];
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,
previousFix,
targetFix,
_leg.TurnDir
);
lineSegments.push({ line });
updateLastCourse(lineSegments.at(-1)!.line);
break;
}
case "CI":
case "CR":
case "DF":
case "FA":
case "FC":
case "FD":
case "FM":
case "HA":
case "HF":
case "HM":
case "IF":
case "PI":
case "RF":
case "TF":
case "VA":
case "VD":
case "VI":
case "VM":
case "VR":
default:
console.error("Unknown TrackCode", leg.TrackCode);
break;
}
}
return geojson.parse([...navFixes, ...lineSegments], {
Point: ["latitude", "longitude"],
LineString: "line",
});
};
}
export default Parser;
+1
View File
@@ -0,0 +1 @@
export const TerminatorsAF = () => {};
@@ -0,0 +1,89 @@
/**
* @param p1 Point 1
* @param brng1 Bearing from Point 1
* @param p2 Point 2
* @param brng2 bearing from Point 2
* @returns Intersection point
*/
export const computeIntersection = (
p1: NavFix,
brng1: number,
p2: NavFix,
brng2: number
): NavFix | null => {
if (isNaN(brng1)) throw new TypeError(`invalid brng1 ${brng1}`);
if (isNaN(brng2)) throw new TypeError(`invalid brng2 ${brng2}`);
const π = Math.PI;
// see www.edwilliams.org/avform.htm#Intersection
const φ1 = p1.latitude.toRadians(),
λ1 = p1.longitude.toRadians();
const φ2 = p2.latitude.toRadians(),
λ2 = p2.longitude.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 { ...p1, latitude: lat, longitude: lon, name: "INTC" };
};
+27
View File
@@ -0,0 +1,27 @@
import { magvar } from "magvar";
Number.prototype.toRadians = function () {
return ((this as number) * Math.PI) / 180;
};
Number.prototype.toDegrees = function () {
return ((this as number) * 180) / Math.PI;
};
Number.prototype.reciprocalCourse = function () {
let inv = (this as number) + 180;
inv = inv >= 360 ? inv - 360 : inv;
return inv;
};
Number.prototype.normaliseDegrees = function () {
return (this as number) >= 360
? (this as number) - 360
: (this as number) < 0
? (this as number) + 360
: (this as number);
};
Number.prototype.toTrue = function (fix) {
const _magvar = magvar(fix.latitude, fix.longitude); //Magvar is returned + for East
return ((this as number) + _magvar).normaliseDegrees();
};
Number.prototype.toMetre = function () {
return (this as number) * 1852.0;
};
@@ -0,0 +1,54 @@
import * as geolib from "geolib";
import Parser from "../parser.ts";
/**
* @param crsIntoEndpoint Course into arc endpoint
* @param crsFromOrigin Course from arc origin point
* @param start Arc origin point
* @param turnDir Turn direction
* @returns Line segments
*/
export const generatePerformanceArc = (
crsIntoEndpoint: number,
crsFromOrigin: number,
start: NavFix,
turnDir?: TurnDirection
) => {
const line: LineSegment[] = [[start.longitude, start.latitude]];
// Check if there even is an arc
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";
}
// Generate arc
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();
}
const arcFix = geolib.computeDestinationPoint(
{
latitude: line.at(-1)![1],
longitude: line.at(-1)![0],
},
(Parser.AC_SPEED / 3600).toMetre(),
crsFromOrigin
);
line.push([arcFix.longitude, arcFix.latitude]);
}
}
return line;
};
@@ -0,0 +1,105 @@
import * as geolib from "geolib";
import { computeIntersection } from "./computeIntersection.ts";
/**
* @param crsIntoEndpoint Course into arc endpoint
* @param crsFromOrigin Course from arc origin point
* @param start Arc origin point
* @param end Leg endpoint
* @param turnDir Turn direction
* @returns Line segments forming arc from `start`to `end`
*/
export const generateTangentArc = (
crsIntoEndpoint: number,
crsFromOrigin: number,
start: NavFix,
end: NavFix,
turnDir?: TurnDirection
) => {
const line: LineSegment[] = [[start.longitude, start.latitude]];
// Check if there even is an arc
if (crsFromOrigin !== crsIntoEndpoint) {
// Course to the end of the arc
let crsFromStartToEnd;
if (!turnDir || turnDir === "E") {
let prov = crsFromOrigin - crsIntoEndpoint;
prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov;
turnDir = prov > 0 ? "L" : "R";
}
if (turnDir === "R") {
const delta = (360 - crsFromOrigin + crsIntoEndpoint).normaliseDegrees();
crsFromStartToEnd = (crsFromOrigin + delta / 2).normaliseDegrees();
} else {
const delta = (crsFromOrigin + 360 - crsIntoEndpoint).normaliseDegrees();
crsFromStartToEnd = (crsFromOrigin - delta / 2).normaliseDegrees();
}
// Arc end
const intcArcOnCrsIntoEndpoint = computeIntersection(
start,
crsFromStartToEnd,
end,
crsIntoEndpoint.reciprocalCourse()
);
if (!intcArcOnCrsIntoEndpoint) return null;
let crsOrthogonalOnOrigin;
let crsOrthogonalOnEndpoint;
if (turnDir === "R") {
crsOrthogonalOnOrigin = (crsFromOrigin + 90).normaliseDegrees();
crsOrthogonalOnEndpoint = (crsIntoEndpoint + 90).normaliseDegrees();
} else {
crsOrthogonalOnOrigin = (crsFromOrigin - 90).normaliseDegrees();
crsOrthogonalOnEndpoint = (crsIntoEndpoint - 90).normaliseDegrees();
}
// Generate arc
const arcCenter = computeIntersection(
start,
crsOrthogonalOnOrigin,
intcArcOnCrsIntoEndpoint,
crsOrthogonalOnEndpoint
);
if (!arcCenter) return null;
const arcRad = geolib.getDistance(arcCenter, 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();
}
if (crsOrthogonalOnOrigin === crsOrthogonalOnEndpoint) break;
const arcFix = geolib.computeDestinationPoint(
arcCenter,
arcRad,
crsOrthogonalOnOrigin
);
line.push([arcFix.longitude, arcFix.latitude]);
}
}
return line;
};
@@ -0,0 +1,68 @@
import * as geolib from "geolib";
import { generateTangentArc } from "./generateTangentArc.ts";
import { generatePerformanceArc } from "./generatePerformanceArc.ts";
/**
* @param crsIntoEndpoint Course into endpoint
* @param crsIntoIntercept Course into intercept point (only fo intercept type legs)
* @param crsFromOrigin Course from arc origin point
* @param start Waypoint where leg starts
* @param end Waypoint where leg ends
* @param turnDir Turn direction
* @returns Line segments from `start` to `end`
*/
export const handleTurnAtFix = (
crsIntoEndpoint: number,
crsIntoIntercept: number,
crsFromOrigin: number,
start: NavFix,
end: NavFix,
turnDir?: TurnDirection
) => {
const line: LineSegment[] = [];
// Overfly turn
if (start.isFlyOver) {
const arc1 = generateTangentArc(
crsIntoEndpoint,
crsFromOrigin,
start,
end,
turnDir
);
const arc2 = generatePerformanceArc(
crsIntoIntercept,
crsFromOrigin,
start,
turnDir
);
// Decide on arc
let arc;
if (arc1) {
const endCrs = geolib.getGreatCircleBearing(
{
latitude: arc1.at(-1)![1],
longitude: arc1.at(-1)![0],
},
end
);
if (endCrs <= crsIntoEndpoint + 1 && endCrs >= crsIntoEndpoint - 1)
arc = arc1;
else arc = arc2;
} else {
arc = arc2;
}
line.push(...arc);
line.push([end.longitude, end.latitude]);
}
// FIXME: Procedural turn
else {
// Direct line for now
line.push([start.longitude, start.latitude], [end.longitude, end.latitude]);
}
return line;
};
+29
View File
@@ -0,0 +1,29 @@
export declare global {
interface Number {
/**
* @returns Value converted from degrees to radians
*/
toRadians: () => number;
/**
* @returns Value converted from radians to degrees
*/
toDegrees: () => number;
/**
* @returns Reciprocal course of value
*/
reciprocalCourse: () => number;
/**
* @returns Normalises to [0,360)
*/
normaliseDegrees: () => number;
/**
* @param fix Fix for magnetic declination
* @returns Value referencing true north
*/
toTrue: (fix: { latitude: number; longitude: number }) => number;
/**
* @returns Value converted from nmi to metres
*/
toMetre: () => number;
}
}
+3
View File
@@ -0,0 +1,3 @@
declare module "geojson" {
export const parse: (data: any, format: any) => any;
}
+3
View File
@@ -0,0 +1,3 @@
declare module "magvar" {
export const magvar: (latitude: number, longitude: number) => number;
}
+18
View File
@@ -0,0 +1,18 @@
export declare global {
type AFTerminalEntry = Required<
Pick<
TerminalEntry,
| "TurnDir"
| "WptID"
| "WptLat"
| "WptLon"
| "NavID"
| "NavLat"
| "NavLon"
| "NavBear"
| "NavDist"
| "Course"
>
> &
TerminalEntry;
}
+18
View File
@@ -0,0 +1,18 @@
export declare global {
type CFTerminalEntry = Required<
Pick<
TerminalEntry,
| "WptID"
| "WptLat"
| "WptLon"
| "NavID"
| "NavLat"
| "NavLon"
| "NavBear"
| "NavDist"
| "Course"
| "Distance"
>
> &
TerminalEntry;
}
+91
View File
@@ -0,0 +1,91 @@
export declare global {
type Waypoint = {
ID: number;
Ident: string;
};
type Runway = {
ID: number;
Latitude: number;
Longitude: number;
Elevation: number;
Ident: string;
TrueHeading: number;
};
type TrackCode =
| "AF"
| "CA"
| "CD"
| "CF"
| "CI"
| "CR"
| "DF"
| "FA"
| "FC"
| "FD"
| "FM"
| "HA"
| "HF"
| "HM"
| "IF"
| "PI"
| "RF"
| "TF"
| "VA"
| "VD"
| "VI"
| "VM"
| "VR";
type TurnDirection = "E" | "L" | "R";
type TerminalEntry = {
ID: number;
TerminalID: number;
Type: string;
Transition: string;
TrackCode: TrackCode;
WptID?: number;
WptLat?: number;
WptLon?: number;
TurnDir?: TurnDirection;
NavID?: number;
NavLat?: number;
NavLon?: number;
NavBear?: number;
NavDist?: number;
Course?: number;
Distance?: number;
Alt?: string;
Vnav: unknown;
CenterID?: number;
CenterLat?: number;
CenterLon?: number;
IsFlyOver: boolean;
SpeedLimit?: string;
IsFAF: boolean;
IsMAP: boolean;
};
type Terminal = {
ID: number;
FullName: string;
ICAO: string;
RwyID: number;
};
type NavFix = {
latitude: number;
longitude: number;
altitude?: number;
speed?: number;
name?: string;
isFlyOver?: boolean;
"marker-color"?: string;
altitudeConstraint?: string;
speedConstraint?: string;
};
type LineSegment = [number, number];
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"typeRoots": ["node_modules/@types", "src/types/**"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"typeRoots": ["node_modules/@types", "src/types/**"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
+281 -43
View File
@@ -1,6 +1,7 @@
import fs from "fs";
import geo from "geojson";
import geolib from "geolib";
import magvar from "magvar";
/* Constants */
const π = Math.PI;
@@ -12,11 +13,13 @@ const AC_VS = 3000;
//const PROCEDURE_ID = 10394;
//const PROCEDURE_ID = 10395;
/* LFRK */
const MAGVAR = 0;
//const MAGVAR = 0;
//const PROCEDURE_ID = 10475;
//const PROCEDURE_ID = 10480;
//const PROCEDURE_ID = 10482;
const PROCEDURE_ID = 10485;
//const PROCEDURE_ID = 10485;
const MAGVAR = -5;
const PROCEDURE_ID = 10653;
/* Prototypes */
Number.prototype.toRadians = function () {
@@ -33,11 +36,18 @@ Number.prototype.flipCourse = function () {
Number.prototype.normaliseDegrees = function () {
return this >= 360 ? this - 360 : this < 0 ? this + 360 : this;
};
Number.prototype.toTrue = function () {
Number.prototype.toTrue = function (fix) {
// IRL Magvar
//return (this - magvar.magvar(fix.lat, fix.lon)).normaliseDegrees();
// NG Magvar
return (this - MAGVAR).normaliseDegrees();
};
/* Functions */
/**
*
* @param {[number, number]} line line segment
*/
const updateLastCourse = (line) => {
lastCourse = geolib.getGreatCircleBearing(
{
@@ -50,7 +60,20 @@ const updateLastCourse = (line) => {
}
);
};
/**
*
* @param {number} nmi Nautical Miles
* @returns Metres
*/
const nmiToMetre = (nmi) => nmi * 1852.0;
/**
*
* @param {{lat: number, lon: number}} p1 Point 1
* @param {number} brng1 Bearing from Point 1
* @param {{lat: number, lon: number}} p2 Point 2
* @param {number} brng2 bearing from Point 2
* @returns {{lat: number, lon: number}} Intersection point
*/
const computeIntersection = (p1, brng1, p2, brng2) => {
if (isNaN(brng1)) throw new TypeError(`invalid brng1 ${brng1}`);
if (isNaN(brng2)) throw new TypeError(`invalid brng2 ${brng2}`);
@@ -126,6 +149,15 @@ const computeIntersection = (p1, brng1, p2, brng2) => {
return { lat, lon, name: "INTC", alt: p1.alt };
};
/**
*
* @param {number} inbdCrs Course into arc endpoint
* @param {*} outbCrs Course into arc origin point
* @param {{lat: number, lon: number}} arcStart Arc origin point
* @param {{lat: number, lon: number}} legEnd Arc endpoint
* @param {"R"|"L"|"E"|null} turnDir Turn direction
* @returns {[[number, number]]} Line segments
*/
const generateTangentialArc = (inbdCrs, outbCrs, arcStart, legEnd, turnDir) => {
const line = [];
@@ -219,6 +251,14 @@ const generateTangentialArc = (inbdCrs, outbCrs, arcStart, legEnd, turnDir) => {
return line;
};
/**
*
* @param {number} inbdCrs Course into arc endpoint
* @param {number} outbCrs Course into arc origin point
* @param {{lat: number, lon: number}} arcStart Arc origin point
* @param {"R"|"L"|"E"|null} turnDir Turn direction
* @returns {[[number, number]]} Line segments
*/
const generatePerformanceArc = (inbdCrs, outbCrs, arcStart, turnDir) => {
const line = [[arcStart.lon, arcStart.lat]];
@@ -260,16 +300,147 @@ const generatePerformanceArc = (inbdCrs, outbCrs, arcStart, turnDir) => {
return line.slice(1);
};
const getCourseAndFixForInterceptions = (leg) => {
/**
*
* @param {number} inbdCrs Course into arc endpoint
* @param {number} outbCrs Course into arc origin point
* @param {{lat: number, lon: number}} arcStart Arc origin point
* @param {{lat: number, lon: number}} center Arc center point
* @param {"R"|"L"|"E"|null} turnDir
* @returns {[[number, number]]} Line segments
*/
const generateRFArc = (inbdCrs, outbCrs, arcStart, center, turnDir) => {
const line = [];
if (inbdCrs !== outbCrs) {
// Turn Dir
if (!turnDir || turnDir === "E") {
let prov = outbCrs - inbdCrs;
prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov;
turnDir = prov > 0 ? "L" : "R";
}
let startPerpCrs;
let endPerpCrs;
if (turnDir === "R") {
startPerpCrs = (outbCrs + 90).normaliseDegrees();
endPerpCrs = (inbdCrs + 90).normaliseDegrees();
} else {
startPerpCrs = (outbCrs - 90).normaliseDegrees();
endPerpCrs = (inbdCrs - 90).normaliseDegrees();
}
const arcRad = geolib.getDistance(
{
latitude: center.lat,
longitude: center.lon,
},
{
latitude: arcStart.lat,
longitude: arcStart.lon,
}
);
startPerpCrs = startPerpCrs.flipCourse();
endPerpCrs = endPerpCrs.flipCourse();
// Start turn immediately
if (turnDir === "R") {
startPerpCrs += startPerpCrs < 1 ? startPerpCrs : 1;
} else {
startPerpCrs -= startPerpCrs < 1 ? startPerpCrs : 1;
}
while (startPerpCrs !== endPerpCrs) {
if (turnDir === "R") {
const delta = (endPerpCrs - startPerpCrs).normaliseDegrees();
startPerpCrs += delta < 1 ? delta : 1;
startPerpCrs = startPerpCrs.normaliseDegrees();
} else {
const delta = (startPerpCrs - endPerpCrs).normaliseDegrees();
startPerpCrs -= delta < 1 ? delta : 1;
startPerpCrs = startPerpCrs.normaliseDegrees();
}
if (startPerpCrs === endPerpCrs) break;
const arcFix = geolib.computeDestinationPoint(
center,
arcRad,
startPerpCrs
);
line.push([arcFix.longitude, arcFix.latitude]);
}
}
return line;
};
/**
*
* @param {number} inbdCrs Course into arc endpoint
* @param {number} outbCrs Course into arc origin point
* @param {{lat: number, lon: number}} arcStart Arc origin point
* @param {{lat: number, lon: number}} center Arc center point
* @param {number} radius Arc radius in nmi
* @param {"R"|"L"|"E"|null} turnDir
* @returns {[[number, number]]} Line segments
*/
const generateAFArc = (inbdCrs, outbCrs, arcStart, center, radius, turnDir) => {
const line = [[arcStart.lon, arcStart.lat]];
if (inbdCrs !== outbCrs) {
// Turn Dir
if (!turnDir || turnDir === "E") {
let prov = outbCrs - inbdCrs;
prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov;
turnDir = prov > 0 ? "L" : "R";
}
while (outbCrs !== inbdCrs) {
if (turnDir === "R") {
const delta = (inbdCrs - outbCrs).normaliseDegrees();
outbCrs += delta < 1 ? delta : 1;
outbCrs = outbCrs.normaliseDegrees();
} else {
const delta = (outbCrs - inbdCrs).normaliseDegrees();
outbCrs -= delta < 1 ? delta : 1;
outbCrs = outbCrs.normaliseDegrees();
}
if (outbCrs === inbdCrs) break;
const arcFix = geolib.computeDestinationPoint(
center,
nmiToMetre(radius),
outbCrs
);
line.push([arcFix.longitude, arcFix.latitude]);
}
}
return line;
};
const getCourseAndFixForInterceptions = (leg, origin) => {
switch (leg.TrackCode) {
case "CF": {
const fix = { lat: leg.WptLat, lon: leg.WptLon };
return [leg.Course.flipCourse().toTrue(fix), fix];
}
case "FM": {
const fix = { lat: leg.WptLat, lon: leg.WptLon };
return [leg.Course.toTrue(fix), fix];
}
case "TF": {
return [
leg.Course.flipCourse().toTrue(),
geolib.getGreatCircleBearing(
{ latitude: origin.lat, longitude: origin.lon },
{ latitude: leg.WptLat, longitude: leg.WptLon }
),
{ lat: leg.WptLat, lon: leg.WptLon },
];
}
case "FM": {
return [leg.Course.toTrue(), { lat: leg.WptLat, lon: leg.WptLon }];
case "AF": {
const fix = { lat: leg.WptLat, lon: leg.WptLon };
return [leg.Course.flipCourse().toTrue(fix), fix];
}
}
};
@@ -322,21 +493,25 @@ const parseAltitude = (alt) => {
};
/* Data */
const waypoints = JSON.parse(fs.readFileSync("Waypoints.json"));
const terminal = JSON.parse(fs.readFileSync("Terminals.json")).filter(
({ ID }) => ID === PROCEDURE_ID
)[0];
const runway = JSON.parse(fs.readFileSync("Runways.json")).filter(
({ ID }) => ID === terminal.RwyID
)[0];
const procedure = JSON.parse(fs.readFileSync(`TermID_${PROCEDURE_ID}.json`));
const waypoints = JSON.parse(
fs.readFileSync("browser/public/navdata/Waypoints.json")
);
const terminal = JSON.parse(
fs.readFileSync("browser/public/navdata/Terminals.json")
).filter(({ ID }) => ID === PROCEDURE_ID)[0];
const runway = JSON.parse(
fs.readFileSync("browser/public/navdata/Runways.json")
).filter(({ ID }) => ID === terminal.RwyID)[0];
const procedure = JSON.parse(
fs.readFileSync(`browser/public/navdata/TermID_${PROCEDURE_ID}.json`)
);
/* Output */
const points = [
{ lat: runway.Latitude, lon: runway.Longitude, alt: runway.Elevation },
];
const lines = [];
let lastCourse = 0;
let lastCourse = runway.TrueHeading;
/* Main */
for (let index = 0; index < procedure.length; index++) {
@@ -344,7 +519,41 @@ for (let index = 0; index < procedure.length; index++) {
const waypoint = waypoints.filter(({ ID }) => ID === leg.WptID)[0];
switch (leg.TrackCode) {
case "AF":
case "AF": {
// Push in ending waypoint
points.push({
lat: leg.WptLat,
lon: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver !== 0 ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver !== 0,
alt: points.at(-1).alt,
});
const arcEndCrs = geolib.getGreatCircleBearing(
{
latitude: leg.NavLat,
longitude: leg.NavLon,
},
{
latitude: leg.WptLat,
longitude: leg.WptLon,
}
);
const line = generateAFArc(
arcEndCrs,
leg.Course.toTrue({ lat: leg.NavLat, lon: leg.NavLon }),
points.at(-2),
{ lat: leg.NavLat, lon: leg.NavLon },
leg.NavDist,
leg.TurnDir
);
lines.push({ line });
updateLastCourse(line);
break;
}
case "CA":
case "CD":
break;
@@ -360,8 +569,8 @@ for (let index = 0; index < procedure.length; index++) {
});
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
leg.Course.toTrue(points.at(-1)),
leg.Course.toTrue(points.at(-1)),
points.at(-2),
points.at(-1),
leg.TurnDir,
@@ -377,20 +586,21 @@ for (let index = 0; index < procedure.length; index++) {
case "CI": {
// Course into the destination fix and said fix
const [inbdCrs, fix] = getCourseAndFixForInterceptions(
procedure[index + 1]
procedure[index + 1],
points.at(-1)
);
// Compute INTC
const intc = computeIntersection(
points.at(-1),
leg.Course.toTrue(),
leg.Course.toTrue(fix),
fix,
inbdCrs
);
const line = handleTurnAtFix(
inbdCrs,
leg.Course.toTrue(),
leg.Course.toTrue(fix),
points.at(-1),
intc,
leg.TurnDir,
@@ -401,7 +611,7 @@ for (let index = 0; index < procedure.length; index++) {
const intc2 = computeIntersection(
{ lat: line.at(-1)[1], lon: line.at(-1)[0] },
leg.Course.toTrue(),
leg.Course.toTrue(fix),
fix,
inbdCrs
);
@@ -420,20 +630,20 @@ for (let index = 0; index < procedure.length; index++) {
}
case "CR": {
// Course into the destination fix
const inbdCrs = leg.Course.toTrue();
const inbdCrs = leg.Course.toTrue(points.at(-1));
// Compute INTC
const intc = computeIntersection(
points.at(-1),
inbdCrs,
{ lat: leg.NavLat, lon: leg.NavLon },
leg.NavBear.toTrue()
leg.NavBear.toTrue({ lat: leg.NavLat, lon: leg.NavLon })
);
points.push(intc);
const line = handleTurnAtFix(
inbdCrs,
leg.Course.toTrue(),
leg.Course.toTrue(points.at(-2)),
points.at(-2),
points.at(-1),
leg.TurnDir,
@@ -457,12 +667,12 @@ for (let index = 0; index < procedure.length; index++) {
longitude: points.at(-1).lon,
},
nmiToMetre(10),
leg.Course.toTrue()
leg.Course.toTrue(points.at(-1))
);
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
leg.Course.toTrue(points.at(-1)),
leg.Course.toTrue(points.at(-1)),
points.at(-1),
{ lat: end.latitude, lon: end.longitude },
leg.TurnDir,
@@ -479,8 +689,35 @@ for (let index = 0; index < procedure.length; index++) {
case "HM":
case "IF":
case "PI":
case "RF":
break;
case "RF": {
// Push in ending waypoint
points.push({
lat: leg.WptLat,
lon: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver !== 0 ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver !== 0,
alt: points.at(-1).alt,
});
const [inbdCrs] = getCourseAndFixForInterceptions(
procedure[index + 1],
points.at(-1)
);
const line = generateRFArc(
inbdCrs,
lastCourse,
points.at(-1),
{ lat: leg.CenterLat, lon: leg.CenterLon },
leg.TurnDir
);
lines.push({ line });
updateLastCourse(line);
break;
}
case "TF": {
// Push in ending waypoint
points.push({
@@ -523,7 +760,7 @@ for (let index = 0; index < procedure.length; index++) {
((parseAltitude(leg.Alt) - points.at(-1).alt) / AC_VS) *
(AC_SPEED / 60)
),
leg.Course.toTrue()
leg.Course.toTrue(points.at(-1))
);
points.push({
lat: end.latitude,
@@ -535,8 +772,8 @@ for (let index = 0; index < procedure.length; index++) {
});
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
leg.Course.toTrue(points.at(-2)),
leg.Course.toTrue(points.at(-2)),
points.at(-2),
{ lat: end.latitude, lon: end.longitude },
leg.TurnDir,
@@ -558,7 +795,7 @@ for (let index = 0; index < procedure.length; index++) {
},
//NOTE: Does not account for slant
nmiToMetre(leg.Distance),
leg.Course.toTrue()
leg.Course.toTrue(points.at(-1))
);
points.push({
lat: end.latitude,
@@ -570,8 +807,8 @@ for (let index = 0; index < procedure.length; index++) {
});
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
leg.Course.toTrue(points.at(-2)),
leg.Course.toTrue(points.at(-2)),
points.at(-2),
{ lat: end.latitude, lon: end.longitude },
leg.TurnDir,
@@ -588,20 +825,21 @@ for (let index = 0; index < procedure.length; index++) {
// Course into the destination fix and said fix
const [inbdCrs, fix] = getCourseAndFixForInterceptions(
procedure[index + 1]
procedure[index + 1],
points.at(-1)
);
// Compute INTC
const intc = computeIntersection(
points.at(-1),
leg.Course.toTrue(),
leg.Course.toTrue(fix),
fix,
inbdCrs
);
const line = handleTurnAtFix(
inbdCrs,
leg.Course.toTrue(),
leg.Course.toTrue(fix),
points.at(-1),
intc,
leg.TurnDir,
@@ -612,7 +850,7 @@ for (let index = 0; index < procedure.length; index++) {
const intc2 = computeIntersection(
{ lat: line.at(-1)[1], lon: line.at(-1)[0] },
leg.Course.toTrue(),
leg.Course.toTrue(fix),
fix,
inbdCrs
);
@@ -631,12 +869,12 @@ for (let index = 0; index < procedure.length; index++) {
longitude: points.at(-1).lon,
},
nmiToMetre(10),
leg.Course.toTrue()
leg.Course.toTrue(points.at(-1))
);
const line = handleTurnAtFix(
leg.Course.toTrue(),
leg.Course.toTrue(),
leg.Course.toTrue(points.at(-1)),
leg.Course.toTrue(points.at(-1)),
points.at(-1),
{ lat: end.latitude, lon: end.longitude },
leg.TurnDir,