Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bdbb583a8 | ||
|
|
2c99b701ce |
No files matched your search
Vendored
+20
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cSpell.words": ["intc"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
@@ -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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+1931
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.
@@ -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='© <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;
|
||||
@@ -0,0 +1,8 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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)));
|
||||
@@ -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;
|
||||
@@ -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" };
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Vendored
+29
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare module "geojson" {
|
||||
export const parse: (data: any, format: any) => any;
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare module "magvar" {
|
||||
export const magvar: (latitude: number, longitude: number) => number;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
export declare global {
|
||||
type AFTerminalEntry = Required<
|
||||
Pick<
|
||||
TerminalEntry,
|
||||
| "TurnDir"
|
||||
| "WptID"
|
||||
| "WptLat"
|
||||
| "WptLon"
|
||||
| "NavID"
|
||||
| "NavLat"
|
||||
| "NavLon"
|
||||
| "NavBear"
|
||||
| "NavDist"
|
||||
| "Course"
|
||||
>
|
||||
> &
|
||||
TerminalEntry;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
export declare global {
|
||||
type CFTerminalEntry = Required<
|
||||
Pick<
|
||||
TerminalEntry,
|
||||
| "WptID"
|
||||
| "WptLat"
|
||||
| "WptLon"
|
||||
| "NavID"
|
||||
| "NavLat"
|
||||
| "NavLon"
|
||||
| "NavBear"
|
||||
| "NavDist"
|
||||
| "Course"
|
||||
| "Distance"
|
||||
>
|
||||
> &
|
||||
TerminalEntry;
|
||||
}
|
||||
Vendored
+91
@@ -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];
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user