Compare commits

..

2 Commits

Author SHA1 Message Date
e4adf30632 Fix Breakpoints 2025-07-16 16:16:50 +02:00
287ad8859b Rebuilt Transition adn chart select 2025-07-16 12:10:01 +02:00
36 changed files with 533 additions and 368 deletions

2
.gitignore vendored
View File

@ -24,3 +24,5 @@ dist-ssr
*.sw? *.sw?
.env .env
NavData/

3
.vscode/launch.json vendored
View File

@ -9,7 +9,8 @@
"request": "launch", "request": "launch",
"name": "Launch Chrome against localhost", "name": "Launch Chrome against localhost",
"url": "http://localhost:3000", "url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/browser" "webRoot": "${workspaceFolder}/browser",
"sourceMaps": true
} }
] ]
} }

View File

@ -530,6 +530,8 @@ LFRN GODA5R SID (cycle 2507, ID 10485)
- Arc center shall be navaid identified by `CenterID`, `CenterLat`, `CenterLon`. - Arc center shall be navaid identified by `CenterID`, `CenterLat`, `CenterLon`.
- Arc and turn shall be flown in direction specified by `TurnDir`. - Arc and turn shall be flown in direction specified by `TurnDir`.
- `Distance` shall be the track miles along the curved path - `Distance` shall be the track miles along the curved path
- `Course` shall be the inbound course of the tangent to the arc at the fix identified by
(`WptID`, `WptLat`, `WptLon`).
### Units ### Units
@ -542,7 +544,6 @@ While similar to an AF, the center point is coded differently.
No radius is specified, but can be inferred based on center point, both endpoints and arc length No radius is specified, but can be inferred based on center point, both endpoints and arc length
Example has `NavBear` set to `null`, significance of the inbound tangential track is unknown. Example has `NavBear` set to `null`, significance of the inbound tangential track is unknown.
Same for the `Course`, which is set, but lacks any documentation.
## Track to Fix (TF) ## Track to Fix (TF)

View File

@ -1,6 +0,0 @@
Revise list of charts to only show those applicable to the selected procedure (so terminal itself and all transitions accompanied)
Revise image overlay
- Find center of full page
- Find center of georeferenced area
- Calculate skew parameters
- Skew geobounds

View File

@ -3,7 +3,8 @@ import { QRCodeSVG } from 'qrcode.react';
import { useState } from 'react'; import { useState } from 'react';
import { ProcedureSelect } from './components//ProcedureSelect'; import { ProcedureSelect } from './components//ProcedureSelect';
import { Map } from './components/Map'; import { Map } from './components/Map';
import { useNavigraphAuth } from './hooks/useNavigraphAuth'; import { Sidebar } from './components/Sidebar';
import { useNavigraphAuth } from './contexts/NavigraphAuth/NavigraphAuthContext';
import Parser from './parser/parser'; import Parser from './parser/parser';
const parser = await Parser.instance(); const parser = await Parser.instance();
@ -12,8 +13,10 @@ function App() {
const [selectedAirport, setSelectedAirport] = useState<Airport>(); const [selectedAirport, setSelectedAirport] = useState<Airport>();
const [selectedRunway, setSelectedRunway] = useState<Runway>(); const [selectedRunway, setSelectedRunway] = useState<Runway>();
const [selectedTerminal, setSelectedTerminal] = useState<Terminal>(); const [selectedTerminal, setSelectedTerminal] = useState<Terminal>();
const [procedures, setProcedures] = useState<{ name: string; data: object }[]>([]); const [transitions, setTransitions] = useState<Procedure[]>([]);
const [params, setParams] = useState<DeviceFlowParams | null>(null); const [params, setParams] = useState<DeviceFlowParams | null>(null);
const [selectedTransition, setSelectedTransition] = useState<Procedure>();
const [selectedChart, setSelectedChart] = useState<Chart>();
const { user, signIn, initialized } = useNavigraphAuth(); const { user, signIn, initialized } = useNavigraphAuth();
@ -21,7 +24,7 @@ function App() {
return ( return (
<> <>
{procedures.length === 0 ? ( {transitions.length === 0 ? (
<div className="flex min-h-dvh w-full"> <div className="flex min-h-dvh w-full">
{!initialized && <div>Loading...</div>} {!initialized && <div>Loading...</div>}
@ -43,29 +46,37 @@ function App() {
setSelectedAirport={setSelectedAirport} setSelectedAirport={setSelectedAirport}
setSelectedRunway={setSelectedRunway} setSelectedRunway={setSelectedRunway}
setSelectedTerminal={setSelectedTerminal} setSelectedTerminal={setSelectedTerminal}
handleSelection={(selectedTransitions) => handleSelection={(selectedTransitions) => {
setProcedures( const _transitions = selectedTransitions.map((transition) => ({
selectedTransitions.map((transition) => ({ name: transition,
name: transition, data: parser.parse(selectedRunway!, transition),
data: parser.parse(selectedRunway!, transition), }));
})) setTransitions(_transitions);
) setSelectedTransition(_transitions[0]);
} setSelectedChart(undefined);
}}
/> />
)} )}
</div> </div>
) : ( ) : (
<div className="flex h-dvh w-dvw"> <div className="flex h-dvh w-dvw">
{procedures.length > 0 && selectedAirport && selectedTerminal ? ( {transitions.length > 0 && selectedAirport && selectedTerminal ? (
<Map <>
airport={selectedAirport} <Sidebar
terminal={selectedTerminal} airport={selectedAirport}
procedures={procedures} terminal={selectedTerminal}
backAction={() => { transitions={transitions}
setSelectedTerminal(undefined); transition={selectedTransition}
setProcedures([]); chart={selectedChart}
}} setTransition={setSelectedTransition}
/> setChart={setSelectedChart}
backAction={() => {
setSelectedTerminal(undefined);
setTransitions([]);
}}
/>
<Map airport={selectedAirport} chart={selectedChart} procedure={selectedTransition} />
</>
) : ( ) : (
<h1 className="text-center text-3xl">Error</h1> <h1 className="text-center text-3xl">Error</h1>
)} )}

View File

@ -1,197 +1,105 @@
import BrowserImageManipulation from 'browser-image-manipulation'; import { default as L } from 'leaflet';
import { default as L, type LatLngBoundsExpression } from 'leaflet';
import 'leaflet-svg-shape-markers'; import 'leaflet-svg-shape-markers';
import { type Chart } from 'navigraph/charts'; import { createRef, type FC } from 'react';
import { createRef, useEffect, useState, type FC } from 'react';
import { GeoJSON, ImageOverlay, MapContainer, TileLayer } from 'react-leaflet'; import { GeoJSON, ImageOverlay, MapContainer, TileLayer } from 'react-leaflet';
import { charts } from '../lib/navigraph';
interface MapProps { interface MapProps {
airport: Airport; airport: Airport;
terminal: Terminal; chart: Chart | undefined;
procedures: { name: string; data: object }[]; procedure: Procedure | undefined;
backAction: () => void;
} }
export const Map: FC<MapProps> = ({ airport, terminal, procedures, backAction }) => { export const Map: FC<MapProps> = ({ airport, chart, procedure }) => {
const [selectedProcedure, setSelectedProcedure] = useState(procedures[0]);
const [chartIndex, setChartIndex] = useState<Chart[]>([]);
const [selectedChart, setSelectedChart] = useState<{
data: string;
index_number: string;
bounds: LatLngBoundsExpression;
}>();
const mapRef = createRef<L.Map>(); const mapRef = createRef<L.Map>();
const imageRef = createRef<L.ImageOverlay>(); const imageRef = createRef<L.ImageOverlay>();
useEffect(() => {
(async () => {
setChartIndex((await charts.getChartsIndex({ icao: airport.ICAO, version: 'STD' })) ?? []);
})();
}, []);
return ( return (
<> <MapContainer
<button center={[airport.Latitude, airport.Longitude]}
className="fixed top-2 left-2 z-[5000] cursor-pointer rounded border border-red-500 bg-red-500 px-2 py-1 font-semibold text-stone-50 focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black" zoom={13}
onClick={backAction} zoomSnap={0}
> className="h-full w-full"
Go back ref={(_mapRef) => {
</button> _mapRef?.attributionControl.setPosition('topright');
_mapRef?.zoomControl.setPosition('topright');
mapRef.current = _mapRef;
}}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapContainer {chart && chart.bounds && (
center={[airport.Latitude, airport.Longitude]} <ImageOverlay
zoom={13} url={chart.data}
zoomSnap={0} bounds={chart.bounds}
className="h-full w-full" opacity={0.75}
ref={(_mapRef) => { ref={(_imageRef) => {
_mapRef?.attributionControl.setPosition('topright'); if (_imageRef) {
_mapRef?.zoomControl.setPosition('topright'); mapRef.current?.fitBounds(_imageRef.getBounds(), {
mapRef.current = _mapRef; padding: [-50, -50],
}}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{selectedChart && selectedChart.bounds && (
<ImageOverlay
url={selectedChart.data}
bounds={selectedChart.bounds}
opacity={0.75}
ref={(_imageRef) => {
if (_imageRef) {
mapRef.current?.fitBounds(_imageRef.getBounds(), {
padding: [-50, -50],
});
}
imageRef.current = _imageRef;
}}
/>
)}
<GeoJSON
key={`${selectedProcedure.name}-lines`}
data={selectedProcedure.data}
style={({ properties }) => ({
color: '#ff00ff',
stroke: true,
weight: 5,
opacity: 1,
dashArray: properties.isManual ? '20, 20' : undefined,
})}
filter={(feature) => feature.geometry.type !== 'Point'}
/>
<GeoJSON
key={`${selectedProcedure.name}-points`}
data={selectedProcedure.data}
style={{
color: 'black',
fill: true,
fillColor: 'transparent',
stroke: true,
weight: 3,
}}
pointToLayer={({ properties }, latlng) => {
if (properties.isFlyOver)
return L.shapeMarker(latlng, {
shape: 'triangle',
radius: 6,
}); });
if (properties.isIntersection) return L.circleMarker(latlng, { radius: 6 }); }
imageRef.current = _imageRef;
return L.shapeMarker(latlng, {
shape: 'star-4',
radius: 10,
rotation: 45,
});
}} }}
onEachFeature={({ geometry, properties }, layer) => { />
if (geometry.type === 'Point') { )}
layer.bindPopup(
`${properties.name}<br> {procedure && (
<>
<GeoJSON
key={`${procedure.name}-lines`}
data={procedure.data}
style={({ properties }) => ({
color: '#ff00ff',
stroke: true,
weight: 5,
opacity: 1,
dashArray: properties.isManual ? '20, 20' : undefined,
})}
filter={(feature) => feature.geometry.type !== 'Point'}
/>
<GeoJSON
key={`${procedure.name}-points`}
data={procedure.data}
style={{
color: 'black',
fill: true,
fillColor: 'transparent',
stroke: true,
weight: 3,
}}
pointToLayer={({ properties }, latlng) => {
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}<br>
${properties.altitude} ft<br> ${properties.altitude} ft<br>
${properties.speed} kts<br> ${properties.speed} kts<br>
CNSTR: CNSTR:
${properties.altitudeConstraint ?? ''} ${properties.altitudeConstraint ?? ''}
${properties.speedConstraint ?? ''}<br>` ${properties.speedConstraint ?? ''}<br>`
); );
} }
}} }}
filter={(feature) => feature.geometry.type === 'Point'} filter={(feature) => feature.geometry.type === 'Point'}
/> />
</MapContainer> </>
)}
<div className="absolute right-0 bottom-0 left-0 z-[5000] bg-[#ffffff77] bg-blend-color backdrop-blur-xs"> </MapContainer>
<div className="flex items-center gap-2 overflow-x-auto p-2">
<span className="text-lg font-semibold">Procedures:</span>
{procedures.map((procedure) => (
<button
key={procedure.name}
className={`cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black ${selectedProcedure.name === procedure.name ? 'outline-2' : ''}`}
onClick={() => {
if (selectedProcedure.name === procedure.name) return;
setSelectedProcedure(procedure);
}}
>
{procedure.name ? procedure.name : 'ZZZZ'}
</button>
))}
</div>
<div className="flex items-center gap-2">
<span className="text-lg font-semibold">Charts:</span>
<div className="flex items-center gap-2 overflow-x-auto p-2">
{chartIndex
.filter((chart) => chart.is_georeferenced)
.map((chart) => (
<button
key={chart.index_number}
className={`cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold whitespace-nowrap focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black ${selectedChart?.index_number === chart.index_number ? 'outline-2' : ''}`}
onClick={async () => {
if (selectedChart?.index_number === chart.index_number) return;
if (!mapRef.current) return;
if (!chart.bounding_boxes) return;
const planView = chart.bounding_boxes.planview;
const chartImage = await charts.getChartImage({ chart, theme: 'light' });
if (!chartImage) return;
// Crop
const dataURL = await new BrowserImageManipulation()
.loadBlob(chartImage)
.crop(
planView.pixels.x2 - planView.pixels.x1,
planView.pixels.y1 - planView.pixels.y2,
planView.pixels.x1,
planView.pixels.y2
)
.saveAsImage();
const bounds = new L.LatLngBounds(
[planView.latlng.lat1, planView.latlng.lng1],
[planView.latlng.lat2, planView.latlng.lng2]
);
setSelectedChart({ data: dataURL, index_number: chart.index_number, bounds });
if (imageRef.current) {
mapRef.current?.fitBounds(imageRef.current.getBounds(), {
padding: [-50, -50],
});
}
}}
>
{chart.name}
</button>
))}
</div>
</div>
</div>
</>
); );
}; };

View File

@ -0,0 +1,109 @@
import BrowserImageManipulation from 'browser-image-manipulation';
import L from 'leaflet';
import type { Chart as NGChart } from 'navigraph/charts';
import { useEffect, useState, type Dispatch, type FC, type SetStateAction } from 'react';
import { charts } from '../lib/navigraph';
interface SidebarProps {
airport: Airport;
terminal: Terminal;
transitions: Procedure[];
transition: Procedure | undefined;
chart: Chart | undefined;
setTransition: Dispatch<SetStateAction<SidebarProps['transition']>>;
setChart: Dispatch<SetStateAction<SidebarProps['chart']>>;
backAction: () => void;
}
export const Sidebar: FC<SidebarProps> = ({
airport,
terminal,
transitions,
transition,
chart,
setTransition,
setChart,
backAction,
}) => {
const [chartIndex, setChartIndex] = useState<NGChart[]>([]);
useEffect(() => {
(async () => {
setChartIndex((await charts.getChartsIndex({ icao: airport.ICAO, version: 'STD' })) ?? []);
})();
}, [airport.ICAO]);
return (
<div className="flex h-full w-[300px] shrink-0 flex-col overflow-hidden">
<button
className="sticky top-0 m-2 cursor-pointer rounded border border-red-500 bg-red-500 px-2 py-1 font-semibold text-stone-50 focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black"
onClick={backAction}
>
Go back
</button>
<div className="flex h-[calc(100%-50px)] flex-col gap-2 overflow-y-auto px-2 pb-2">
<div className="flex flex-col gap-2">
<div className="sticky top-0 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">
Transitions for <span className="font-bold">{terminal.FullName}</span>
</div>
{transitions.map((_procedure) => (
<button
key={_procedure.name}
className={`cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black ${_procedure.name === transition?.name ? 'outline-2' : ''}`}
onClick={() => {
if (_procedure.name === transition?.name) return;
setTransition(_procedure);
}}
>
{_procedure.name ? _procedure.name : 'ZZZZ'}
</button>
))}
</div>
<div className="flex flex-col gap-2">
<div className="sticky top-0 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">Charts</div>
{chartIndex
.filter((_chart) => _chart.is_georeferenced)
.map((_chart) => (
<button
key={_chart.index_number}
className={`cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 text-left font-semibold focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black ${chart?.index_number === _chart.index_number ? 'outline-2' : ''}`}
onClick={async () => {
if (chart?.index_number === _chart.index_number) return;
if (!_chart.bounding_boxes) return;
const planView = _chart.bounding_boxes.planview;
const chartImage = await charts.getChartImage({ chart: _chart, theme: 'light' });
if (!chartImage) return;
// Crop
const dataURL = await new BrowserImageManipulation()
.loadBlob(chartImage)
.crop(
planView.pixels.x2 - planView.pixels.x1,
planView.pixels.y1 - planView.pixels.y2,
planView.pixels.x1,
planView.pixels.y2
)
.saveAsImage();
const bounds = new L.LatLngBounds(
[planView.latlng.lat1, planView.latlng.lng1],
[planView.latlng.lat2, planView.latlng.lng2]
);
setChart({ data: dataURL, index_number: _chart.index_number, bounds });
}}
>
<span className="font-bold">{_chart.index_number}</span>
<br />
{_chart.name}
</button>
))}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,19 @@
import { type User } from 'navigraph/auth';
import { createContext, useContext } from 'react';
import { auth } from '../../lib/navigraph';
interface NavigraphAuthContext {
initialized: boolean;
user: User | null;
signIn: typeof auth.signInWithDeviceFlow;
}
export const authContext = createContext<NavigraphAuthContext>({
initialized: false,
user: null,
signIn: () => Promise.reject('Not initialized'),
});
export const useNavigraphAuth = () => {
return useContext(authContext);
};

View File

@ -1,18 +1,7 @@
import { type User } from 'navigraph/auth'; import type { User } from 'navigraph/auth';
import React, { createContext, useContext, useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { auth } from '../lib/navigraph'; import { auth } from '../../lib/navigraph';
import { authContext } from './NavigraphAuthContext';
interface NavigraphAuthContext {
initialized: boolean;
user: User | null;
signIn: typeof auth.signInWithDeviceFlow;
}
const authContext = createContext<NavigraphAuthContext>({
initialized: false,
user: null,
signIn: () => Promise.reject('Not initialized'),
});
// Provider hook that creates auth object and handles state // Provider hook that creates auth object and handles state
function useProvideAuth() { function useProvideAuth() {
@ -42,12 +31,7 @@ function useProvideAuth() {
// Provider component that wraps your app and makes auth object // Provider component that wraps your app and makes auth object
// available to any child component that calls useAuth(). // available to any child component that calls useAuth().
export function NavigraphAuthProvider({ children }: { children: React.ReactNode }) { export function NavigraphAuthProvider({ children }: { children: React.ReactNode }) {
const auth = useProvideAuth(); const _auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>; return <authContext.Provider value={_auth}>{children}</authContext.Provider>;
} }
// Hook for child components to get the auth object
// and re-render when it changes.
export const useNavigraphAuth = () => {
return useContext(authContext);
};

View File

@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
import App from './App.tsx'; import App from './App.tsx';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import { NavigraphAuthProvider } from './hooks/useNavigraphAuth.tsx'; import { NavigraphAuthProvider } from './contexts/NavigraphAuth/NavigraphAuthProvider.tsx';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>

View File

@ -1,5 +1,5 @@
import geojson from 'geojson'; import geojson from 'geojson';
import * as geolib from 'geolib'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import { TerminatorsAF } from './terminators/AF'; import { TerminatorsAF } from './terminators/AF';
import { TerminatorsCA } from './terminators/CA'; import { TerminatorsCA } from './terminators/CA';
import { TerminatorsCD } from './terminators/CD'; import { TerminatorsCD } from './terminators/CD';
@ -90,7 +90,7 @@ class Parser {
* @param line Line segments * @param line Line segments
*/ */
const updateLastCourse = (line: LineSegment[]) => { const updateLastCourse = (line: LineSegment[]) => {
lastCourse = geolib.getGreatCircleBearing( lastCourse = getGreatCircleBearing(
{ {
latitude: line.at(-2)![1], latitude: line.at(-2)![1],
longitude: line.at(-2)![0], longitude: line.at(-2)![0],
@ -153,7 +153,12 @@ class Parser {
break; break;
} }
case 'CF': { case 'CF': {
const [fixToAdd, lineToAdd] = TerminatorsCF(leg as CFTerminalEntry, previousFix, lastCourse, waypoint); const [fixToAdd, lineToAdd] = TerminatorsCF(
leg as CFTerminalEntry,
{ ...previousFix }, // COPY
lastCourse,
waypoint
);
update(fixToAdd, lineToAdd); update(fixToAdd, lineToAdd);
break; break;
} }
@ -161,7 +166,7 @@ class Parser {
const [fixToAdd, lineToAdd] = TerminatorsCI( const [fixToAdd, lineToAdd] = TerminatorsCI(
leg as CITerminalEntry, leg as CITerminalEntry,
procedure[index + 1], procedure[index + 1],
previousFix, { ...previousFix }, // COPY
lastCourse lastCourse
); );
update(fixToAdd, lineToAdd); update(fixToAdd, lineToAdd);
@ -212,14 +217,14 @@ class Parser {
console.error('Unknown TrackCode', leg.TrackCode); console.error('Unknown TrackCode', leg.TrackCode);
break; break;
case 'RF': { case 'RF': {
const [fixToAdd, lineToAdd] = TerminatorsRF( const [fixToAdd, lineToAdd] = TerminatorsRF(leg as RFTerminalEntry, previousFix, lastCourse, waypoint);
leg as RFTerminalEntry, if (fixToAdd) {
procedure[index + 1], navFixes.push(fixToAdd);
previousFix, lastCourse = (leg as RFTerminalEntry).Course?.toTrue(fixToAdd);
lastCourse, }
waypoint if (lineToAdd) {
); lineSegments.push({ line: lineToAdd });
update(fixToAdd, lineToAdd); }
break; break;
} }
case 'TF': { case 'TF': {
@ -241,7 +246,7 @@ class Parser {
const [fixToAdd, lineToAdd] = TerminatorsVI( const [fixToAdd, lineToAdd] = TerminatorsVI(
leg as VITerminalEntry, leg as VITerminalEntry,
procedure[index + 1], procedure[index + 1],
previousFix, { ...previousFix }, // COPY
lastCourse lastCourse
); );
update(fixToAdd, lineToAdd); update(fixToAdd, lineToAdd);

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
/** /**
* @param crsIntoEndpoint Course into arc endpoint * @param crsIntoEndpoint Course into arc endpoint
@ -39,7 +39,7 @@ export const generateAFArc = (
} }
if (crsFromOrigin === crsIntoEndpoint) break; if (crsFromOrigin === crsIntoEndpoint) break;
const arcFix = geolib.computeDestinationPoint(center, radius.toMetre(), crsFromOrigin); const arcFix = computeDestinationPoint(center, radius.toMetre(), crsFromOrigin);
line.push([arcFix.longitude, arcFix.latitude]); line.push([arcFix.longitude, arcFix.latitude]);
} }

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import { generatePerformanceArc } from './generatePerformanceArc'; import { generatePerformanceArc } from './generatePerformanceArc';
/** /**
@ -31,7 +31,7 @@ export const generateOverflyArc = (
// Get arc endpoint and crs into arc endpoint // Get arc endpoint and crs into arc endpoint
const arcEnd = { latitude: line.at(-1)![1], longitude: line.at(-1)![0] }; const arcEnd = { latitude: line.at(-1)![1], longitude: line.at(-1)![0] };
if (line.length > 1) { if (line.length > 1) {
crsFromOrigin = geolib.getGreatCircleBearing( crsFromOrigin = getGreatCircleBearing(
{ {
latitude: line.at(-2)![1], latitude: line.at(-2)![1],
longitude: line.at(-2)![0], longitude: line.at(-2)![0],

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import Parser from '../parser'; import Parser from '../parser';
import { computeTurnRate } from '../utils/computeTurnRate'; import { computeTurnRate } from '../utils/computeTurnRate';
@ -48,7 +48,7 @@ export const generatePerformanceArc = (
time = increment / turnRate; time = increment / turnRate;
} }
const arcFix = geolib.computeDestinationPoint( const arcFix = computeDestinationPoint(
{ {
latitude: line.at(-1)![1], latitude: line.at(-1)![1],
longitude: line.at(-1)![0], longitude: line.at(-1)![0],
@ -80,7 +80,7 @@ export const generatePerformanceArc = (
time = increment / turnRate; time = increment / turnRate;
} }
const arcFix = geolib.computeDestinationPoint( const arcFix = computeDestinationPoint(
{ {
latitude: line.at(-1)![1], latitude: line.at(-1)![1],
longitude: line.at(-1)![0], longitude: line.at(-1)![0],

View File

@ -1,4 +1,5 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getDistance from 'geolib/es/getDistance';
/** /**
* @param crsIntoEndpoint Course into arc endpoint * @param crsIntoEndpoint Course into arc endpoint
@ -17,7 +18,7 @@ export const generateRFArc = (
) => { ) => {
const line: LineSegment[] = [[start.longitude, start.latitude]]; const line: LineSegment[] = [[start.longitude, start.latitude]];
if (crsIntoEndpoint !== crsIntoOrigin) { if (!crsIntoEndpoint.equal(crsIntoOrigin)) {
// Turn Dir // Turn Dir
if (!turnDir || turnDir === 'E') { if (!turnDir || turnDir === 'E') {
let prov = crsIntoOrigin - crsIntoEndpoint; let prov = crsIntoOrigin - crsIntoEndpoint;
@ -35,7 +36,7 @@ export const generateRFArc = (
crsOrthogonalOnEndpoint = (crsIntoEndpoint - 90).normaliseDegrees(); crsOrthogonalOnEndpoint = (crsIntoEndpoint - 90).normaliseDegrees();
} }
const arcRad = geolib.getDistance(center, start); const arcRad = getDistance(center, start);
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse();
crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse(); crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse();
@ -46,7 +47,7 @@ export const generateRFArc = (
crsOrthogonalOnOrigin -= crsOrthogonalOnOrigin < 1 ? crsOrthogonalOnOrigin : 1; crsOrthogonalOnOrigin -= crsOrthogonalOnOrigin < 1 ? crsOrthogonalOnOrigin : 1;
} }
while (crsOrthogonalOnOrigin !== crsOrthogonalOnEndpoint) { while (!crsOrthogonalOnOrigin.equal(crsOrthogonalOnEndpoint)) {
if (turnDir === 'R') { if (turnDir === 'R') {
const delta = (crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin).normaliseDegrees(); const delta = (crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin).normaliseDegrees();
crsOrthogonalOnOrigin += delta < 1 ? delta : 1; crsOrthogonalOnOrigin += delta < 1 ? delta : 1;
@ -57,7 +58,7 @@ export const generateRFArc = (
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees();
} }
const arcFix = geolib.computeDestinationPoint(center, arcRad, crsOrthogonalOnOrigin); const arcFix = computeDestinationPoint(center, arcRad, crsOrthogonalOnOrigin);
line.push([arcFix.longitude, arcFix.latitude]); line.push([arcFix.longitude, arcFix.latitude]);
} }

View File

@ -1,4 +1,5 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getDistance from 'geolib/es/getDistance';
import { computeIntersection } from '../utils/computeIntersection'; import { computeIntersection } from '../utils/computeIntersection';
/** /**
@ -62,7 +63,7 @@ export const generateTangentArc = (
crsOrthogonalOnEndpoint crsOrthogonalOnEndpoint
); );
if (!arcCenter) return null; if (!arcCenter) return null;
const arcRad = geolib.getDistance(arcCenter, start); const arcRad = getDistance(arcCenter, start);
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse();
crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse(); crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse();
@ -84,7 +85,7 @@ export const generateTangentArc = (
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees();
} }
const arcFix = geolib.computeDestinationPoint(arcCenter, arcRad, crsOrthogonalOnOrigin); const arcFix = computeDestinationPoint(arcCenter, arcRad, crsOrthogonalOnOrigin);
line.push([arcFix.longitude, arcFix.latitude]); line.push([arcFix.longitude, arcFix.latitude]);
} }

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import { generatePerformanceArc } from './generatePerformanceArc'; import { generatePerformanceArc } from './generatePerformanceArc';
import { generateTangentArc } from './generateTangentArc'; import { generateTangentArc } from './generateTangentArc';
@ -32,7 +32,7 @@ export const handleTurnAtFix = (
// Decide on arc // Decide on arc
let arc; let arc;
if (arc1) { if (arc1) {
const endCrs = geolib.getGreatCircleBearing( const endCrs = getGreatCircleBearing(
{ {
latitude: arc1.at(-1)![1], latitude: arc1.at(-1)![1],
longitude: arc1.at(-1)![0], longitude: arc1.at(-1)![0],
@ -48,9 +48,8 @@ export const handleTurnAtFix = (
line.push(...arc); line.push(...arc);
line.push([end.longitude, end.latitude]); line.push([end.longitude, end.latitude]);
} }
// FIXME: Procedural turn // Procedural turn
else { else {
// Direct line for now
line.push([start.longitude, start.latitude], [end.longitude, end.latitude]); line.push([start.longitude, start.latitude], [end.longitude, end.latitude]);
} }

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import { generateAFArc } from '../pathGenerators/generateAFArc'; import { generateAFArc } from '../pathGenerators/generateAFArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -18,7 +18,7 @@ export const TerminatorsAF = (
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
}; };
const arcEndCrs = geolib.getGreatCircleBearing( const arcEndCrs = getGreatCircleBearing(
{ {
latitude: leg.NavLat, latitude: leg.NavLat,
longitude: leg.NavLon, longitude: leg.NavLon,

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import Parser from '../parser'; import Parser from '../parser';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -17,7 +17,7 @@ export const TerminatorsCA = (
// Compute intercept of crs from arc end and expected altitude // Compute intercept of crs from arc end and expected altitude
const targetFix: NavFix = { const targetFix: NavFix = {
...geolib.computeDestinationPoint( ...computeDestinationPoint(
arcEnd, arcEnd,
( (
((leg.Alt.parseAltitude() - (previousFix.altitude ?? 0)) / Parser.AC_VS) * ((leg.Alt.parseAltitude() - (previousFix.altitude ?? 0)) / Parser.AC_VS) *

View File

@ -1,4 +1,6 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getDistance from 'geolib/es/getDistance';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -20,8 +22,8 @@ export const TerminatorsCD = (
lastCourse = _lastCourse; lastCourse = _lastCourse;
// Compute distance to fly from arc end // Compute distance to fly from arc end
const crsToNavaid = geolib.getGreatCircleBearing(arcEnd, navaid); const crsToNavaid = getGreatCircleBearing(arcEnd, navaid);
const distToNavaid = geolib.getDistance(arcEnd, navaid); const distToNavaid = getDistance(arcEnd, navaid);
let remainingDistance = leg.Distance.toMetre(); let remainingDistance = leg.Distance.toMetre();
// Navaid behind us // Navaid behind us
if (Math.abs(crsToNavaid - lastCourse) > 90) { if (Math.abs(crsToNavaid - lastCourse) > 90) {
@ -35,7 +37,7 @@ export const TerminatorsCD = (
// Compute intercept of crs from arc end and distance // Compute intercept of crs from arc end and distance
const targetFix: NavFix = { const targetFix: NavFix = {
...geolib.computeDestinationPoint(arcEnd, remainingDistance, lastCourse), ...computeDestinationPoint(arcEnd, remainingDistance, lastCourse),
name: leg.Distance.toString(), name: leg.Distance.toString(),
isFlyOver: true, isFlyOver: true,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,

View File

@ -1,5 +1,9 @@
import { handleTurnAtFix } from '../pathGenerators/handleTurnAtFix'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import Parser from '../parser';
import { computeIntersection } from '../utils/computeIntersection';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
import { computeTurnRate } from '../utils/computeTurnRate';
export const TerminatorsCF = ( export const TerminatorsCF = (
leg: CFTerminalEntry, leg: CFTerminalEntry,
@ -8,6 +12,8 @@ export const TerminatorsCF = (
waypoint?: Waypoint waypoint?: Waypoint
): [NavFix?, LineSegment[]?] => { ): [NavFix?, LineSegment[]?] => {
const speed = computeSpeed(leg, previousFix); const speed = computeSpeed(leg, previousFix);
const crsIntoEndpoint = leg.Course.toTrue(previousFix);
const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]];
const targetFix: NavFix = { const targetFix: NavFix = {
latitude: leg.WptLat, latitude: leg.WptLat,
@ -19,17 +25,70 @@ export const TerminatorsCF = (
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
}; };
const crsToIntercept = leg.Course.toTrue(targetFix);
// Compute arc // Compute overfly arc
const line = handleTurnAtFix( if (previousFix.isFlyOver && !lastCourse.equal(crsIntoEndpoint)) {
leg.Course.toTrue(previousFix), const turnRate = computeTurnRate(speed, Parser.AC_BANK);
leg.Course.toTrue(previousFix), let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
lastCourse,
previousFix, // Turn Dir
targetFix, if (!leg.TurnDir || leg.TurnDir === 'E') {
speed, let prov = lastCourse - crsIntoEndpoint;
leg.TurnDir prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov;
); leg.TurnDir = prov > 0 ? 'L' : 'R';
}
// Generate arc
while (!updatedCrsToIntercept.equal(crsToIntercept)) {
let time = 0;
if (leg.TurnDir === 'R') {
//const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
const increment = 1; //delta < 1 ? delta : 1;
lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate;
} else {
//const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
const increment = 1; //delta < 1 ? delta : 1;
lastCourse = (lastCourse - increment).normaliseDegrees();
time = increment / turnRate;
}
const arcFix = computeDestinationPoint(
{
latitude: line.at(-1)![1],
longitude: line.at(-1)![0],
},
((speed / 3600) * time).toMetre(),
lastCourse
);
line.push([arcFix.longitude, arcFix.latitude]);
// Update previousFix
previousFix.latitude = arcFix.latitude;
previousFix.longitude = arcFix.longitude;
updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
let interceptAngle = 0;
if (leg.TurnDir === 'R') Math.abs((interceptAngle = lastCourse - crsToIntercept));
else interceptAngle = Math.abs(crsToIntercept - lastCourse);
if (interceptAngle >= 45) break;
}
}
const interceptFix: NavFix = {
...computeIntersection(previousFix, leg.Course.toTrue(previousFix), targetFix, crsToIntercept.reciprocalCourse())!,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: speed,
speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt,
};
if (interceptFix.latitude) line.push([interceptFix.longitude, interceptFix.latitude]);
line.push([targetFix.longitude, targetFix.latitude]);
return [targetFix, line]; return [targetFix, line];
}; };

View File

@ -1,6 +1,9 @@
import { handleTurnAtFix } from '../pathGenerators/handleTurnAtFix'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import Parser from '../parser';
import { computeIntersection } from '../utils/computeIntersection'; import { computeIntersection } from '../utils/computeIntersection';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
import { computeTurnRate } from '../utils/computeTurnRate';
import { getCourseAndFixForIntercepts } from '../utils/getCourseAndFixForIntercepts'; import { getCourseAndFixForIntercepts } from '../utils/getCourseAndFixForIntercepts';
export const TerminatorsCI = ( export const TerminatorsCI = (
@ -9,12 +12,57 @@ export const TerminatorsCI = (
previousFix: NavFix, previousFix: NavFix,
lastCourse: number lastCourse: number
): [NavFix?, LineSegment[]?] => { ): [NavFix?, LineSegment[]?] => {
const [crs, nextFix] = getCourseAndFixForIntercepts(nextLeg, previousFix);
const speed = computeSpeed(leg, previousFix); const speed = computeSpeed(leg, previousFix);
const crsIntoEndpoint = leg.Course.toTrue(previousFix);
const [crsToIntercept, nextFix] = getCourseAndFixForIntercepts(nextLeg, previousFix);
const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]];
// Compute overfly arc
if (previousFix.isFlyOver && !lastCourse.equal(crsIntoEndpoint)) {
const turnRate = computeTurnRate(speed, Parser.AC_BANK);
const updatedCrsToIntercept = getGreatCircleBearing(previousFix, nextFix);
// 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
while (!lastCourse.equal(crsIntoEndpoint) && !updatedCrsToIntercept.equal(crsToIntercept)) {
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 = computeDestinationPoint(
{
latitude: line.at(-1)![1],
longitude: line.at(-1)![0],
},
((speed / 3600) * time).toMetre(),
lastCourse
);
line.push([arcFix.longitude, arcFix.latitude]);
// Update previousFix
previousFix.latitude = arcFix.latitude;
previousFix.longitude = arcFix.longitude;
}
}
// Compute intercept fix
const interceptFix: NavFix = { const interceptFix: NavFix = {
...computeIntersection(previousFix, leg.Course.toTrue(nextFix), nextFix, crs)!, ...computeIntersection(previousFix, leg.Course.toTrue(nextFix), nextFix, crsToIntercept)!,
isFlyOver: leg.IsFlyOver, isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: speed, speed: speed,
@ -22,29 +70,7 @@ export const TerminatorsCI = (
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
}; };
// Compute arc line.push([interceptFix.longitude, interceptFix.latitude]);
const line = handleTurnAtFix(
crs,
leg.Course.toTrue(nextFix),
lastCourse,
previousFix,
interceptFix,
speed,
leg.TurnDir
);
// Recompute intercept
const interceptPoint2 = computeIntersection(
{ latitude: line.at(-2)![1], longitude: line.at(-2)![0] },
leg.Course.toTrue(nextFix),
nextFix,
crs
);
if (interceptPoint2)
return [
{ ...interceptFix, ...interceptPoint2 },
[...line.slice(0, -1), [interceptPoint2.longitude, interceptPoint2.latitude]],
];
return [interceptFix, line]; return [interceptFix, line];
}; };

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -21,7 +21,7 @@ export const TerminatorsDF = (
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
}; };
const crsIntoEndpoint = geolib.getGreatCircleBearing(previousFix, targetFix); const crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix);
// Compute overfly // Compute overfly
const [line, _, _lastCourse] = generateOverflyArc( const [line, _, _lastCourse] = generateOverflyArc(

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import Parser from '../parser'; import Parser from '../parser';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -21,7 +21,7 @@ export const TerminatorsFA = (
// Compute intercept of crs from arc end and expected altitude // Compute intercept of crs from arc end and expected altitude
const targetFix: NavFix = { const targetFix: NavFix = {
...geolib.computeDestinationPoint( ...computeDestinationPoint(
arcEnd, arcEnd,
( (
((leg.Alt.parseAltitude() - (previousFix.altitude ?? 0)) / Parser.AC_VS) * ((leg.Alt.parseAltitude() - (previousFix.altitude ?? 0)) / Parser.AC_VS) *

View File

@ -1,7 +1,8 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import Parser from '../parser'; import Parser from '../parser';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
import { computeTurnRate } from '../utils/computeTurnRate'; import { computeTurnRate } from '../utils/computeTurnRate';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
// NOTE: Distance not adjusted for altitude in this demo // NOTE: Distance not adjusted for altitude in this demo
export const TerminatorsFC = ( export const TerminatorsFC = (
@ -47,7 +48,7 @@ export const TerminatorsFC = (
time = increment / turnRate; time = increment / turnRate;
} }
const arcFix = geolib.computeDestinationPoint( const arcFix = computeDestinationPoint(
{ {
latitude: line.at(-1)![1], latitude: line.at(-1)![1],
longitude: line.at(-1)![0], longitude: line.at(-1)![0],
@ -69,7 +70,7 @@ export const TerminatorsFC = (
const arcEnd = { latitude: line.at(-1)![1], longitude: line.at(-1)![0] }; const arcEnd = { latitude: line.at(-1)![1], longitude: line.at(-1)![0] };
if (line.length > 1) { if (line.length > 1) {
lastCourse = geolib.getGreatCircleBearing( lastCourse = getGreatCircleBearing(
{ {
latitude: line.at(-2)![1], latitude: line.at(-2)![1],
longitude: line.at(-2)![0], longitude: line.at(-2)![0],
@ -79,7 +80,7 @@ export const TerminatorsFC = (
} }
const targetFix: NavFix = { const targetFix: NavFix = {
...geolib.computeDestinationPoint(arcEnd, leg.Distance.toMetre(), lastCourse), ...computeDestinationPoint(arcEnd, leg.Distance.toMetre(), lastCourse),
name: leg.Distance.toString(), name: leg.Distance.toString(),
isFlyOver: true, isFlyOver: true,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,

View File

@ -1,4 +1,6 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getDistance from 'geolib/es/getDistance';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -24,8 +26,8 @@ export const TerminatorsFD = (
lastCourse = _lastCourse; lastCourse = _lastCourse;
// Compute distance to fly from arc end // Compute distance to fly from arc end
const crsToNavaid = geolib.getGreatCircleBearing(arcEnd, navaid); const crsToNavaid = getGreatCircleBearing(arcEnd, navaid);
const distToNavaid = geolib.getDistance(arcEnd, navaid); const distToNavaid = getDistance(arcEnd, navaid);
let remainingDistance = leg.Distance.toMetre(); let remainingDistance = leg.Distance.toMetre();
// Navaid behind us // Navaid behind us
if (Math.abs(crsToNavaid - lastCourse) > 90) { if (Math.abs(crsToNavaid - lastCourse) > 90) {
@ -39,7 +41,7 @@ export const TerminatorsFD = (
// Compute intercept of crs from arc end and distance // Compute intercept of crs from arc end and distance
const targetFix: NavFix = { const targetFix: NavFix = {
...geolib.computeDestinationPoint(arcEnd, remainingDistance, lastCourse), ...computeDestinationPoint(arcEnd, remainingDistance, lastCourse),
name: leg.Distance.toString(), name: leg.Distance.toString(),
isFlyOver: true, isFlyOver: true,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import { handleTurnAtFix } from '../pathGenerators/handleTurnAtFix'; import { handleTurnAtFix } from '../pathGenerators/handleTurnAtFix';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -9,7 +9,7 @@ export const TerminatorsFM = (
): [NavFix?, LineSegment[]?] => { ): [NavFix?, LineSegment[]?] => {
const speed = computeSpeed(leg, previousFix); const speed = computeSpeed(leg, previousFix);
const endpoint = geolib.computeDestinationPoint(previousFix, (10).toMetre(), leg.Course.toTrue(previousFix)); const endpoint = computeDestinationPoint(previousFix, (10).toMetre(), leg.Course.toTrue(previousFix));
const line = handleTurnAtFix( const line = handleTurnAtFix(
leg.Course.toTrue(previousFix), leg.Course.toTrue(previousFix),

View File

@ -1,10 +1,8 @@
import { generateRFArc } from '../pathGenerators/generateRFArc'; import { generateRFArc } from '../pathGenerators/generateRFArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
import { getCourseAndFixForIntercepts } from '../utils/getCourseAndFixForIntercepts';
export const TerminatorsRF = ( export const TerminatorsRF = (
leg: RFTerminalEntry, leg: RFTerminalEntry,
nextLeg: TerminalEntry,
previousFix: NavFix, previousFix: NavFix,
lastCourse: number, lastCourse: number,
waypoint?: Waypoint waypoint?: Waypoint
@ -20,10 +18,8 @@ export const TerminatorsRF = (
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
}; };
const [crs] = getCourseAndFixForIntercepts(nextLeg, previousFix);
const line = generateRFArc( const line = generateRFArc(
crs, leg.Course.toTrue(targetFix),
lastCourse, lastCourse,
previousFix, previousFix,
{ latitude: leg.CenterLat, longitude: leg.CenterLon }, { latitude: leg.CenterLat, longitude: leg.CenterLon },

View File

@ -1,6 +1,7 @@
import * as geolib from 'geolib'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import Parser from '../parser'; import Parser from '../parser';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
export const TerminatorsTF = ( export const TerminatorsTF = (
leg: TFTerminalEntry, leg: TFTerminalEntry,
@ -21,7 +22,7 @@ export const TerminatorsTF = (
const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]];
const trackIntoEndpoint = geolib.getGreatCircleBearing(previousFix, targetFix); const trackIntoEndpoint = getGreatCircleBearing(previousFix, targetFix);
if (previousFix.isFlyOver) { if (previousFix.isFlyOver) {
let crsIntoEndpoint = trackIntoEndpoint; let crsIntoEndpoint = trackIntoEndpoint;
@ -48,7 +49,7 @@ export const TerminatorsTF = (
lastCourse = lastCourse.normaliseDegrees(); lastCourse = lastCourse.normaliseDegrees();
} }
const arcFix = geolib.computeDestinationPoint( const arcFix = computeDestinationPoint(
{ {
latitude: line.at(-1)![1], latitude: line.at(-1)![1],
longitude: line.at(-1)![0], longitude: line.at(-1)![0],
@ -59,7 +60,7 @@ export const TerminatorsTF = (
line.push([arcFix.longitude, arcFix.latitude]); line.push([arcFix.longitude, arcFix.latitude]);
crsIntoEndpoint = geolib.getGreatCircleBearing(arcFix, targetFix); crsIntoEndpoint = getGreatCircleBearing(arcFix, targetFix);
if (leg.TurnDir === 'R') { if (leg.TurnDir === 'R') {
condition = crsIntoEndpoint > trackIntoEndpoint; condition = crsIntoEndpoint > trackIntoEndpoint;

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import Parser from '../parser'; import Parser from '../parser';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -18,7 +18,7 @@ export const TerminatorsVA = (
// Compute intercept of crs from arc end and expected altitude // Compute intercept of crs from arc end and expected altitude
const targetFix: NavFix = { const targetFix: NavFix = {
...geolib.computeDestinationPoint( ...computeDestinationPoint(
arcEnd, arcEnd,
( (
((leg.Alt.parseAltitude() - (previousFix.altitude ?? 0)) / Parser.AC_VS) * ((leg.Alt.parseAltitude() - (previousFix.altitude ?? 0)) / Parser.AC_VS) *

View File

@ -1,4 +1,6 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getDistance from 'geolib/es/getDistance';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -21,8 +23,8 @@ export const TerminatorsVD = (
lastCourse = _lastCourse; lastCourse = _lastCourse;
// Compute distance to fly from arc end // Compute distance to fly from arc end
const crsToNavaid = geolib.getGreatCircleBearing(arcEnd, navaid); const crsToNavaid = getGreatCircleBearing(arcEnd, navaid);
const distToNavaid = geolib.getDistance(arcEnd, navaid); const distToNavaid = getDistance(arcEnd, navaid);
let remainingDistance = leg.Distance.toMetre(); let remainingDistance = leg.Distance.toMetre();
// Navaid behind us // Navaid behind us
if (Math.abs(crsToNavaid - lastCourse) > 90) { if (Math.abs(crsToNavaid - lastCourse) > 90) {
@ -36,7 +38,7 @@ export const TerminatorsVD = (
// Compute intercept of crs from arc end and distance // Compute intercept of crs from arc end and distance
const targetFix: NavFix = { const targetFix: NavFix = {
...geolib.computeDestinationPoint(arcEnd, remainingDistance, lastCourse), ...computeDestinationPoint(arcEnd, remainingDistance, lastCourse),
name: leg.Distance.toString(), name: leg.Distance.toString(),
isFlyOver: true, isFlyOver: true,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,

View File

@ -1,7 +1,10 @@
import { handleTurnAtFix } from '../pathGenerators/handleTurnAtFix'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import Parser from '../parser';
import { computeIntersection } from '../utils/computeIntersection'; import { computeIntersection } from '../utils/computeIntersection';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
import { computeTurnRate } from '../utils/computeTurnRate';
import { getCourseAndFixForIntercepts } from '../utils/getCourseAndFixForIntercepts'; import { getCourseAndFixForIntercepts } from '../utils/getCourseAndFixForIntercepts';
import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
// NOTE: No wind adjustments to be made, no clue how *that* would draw // NOTE: No wind adjustments to be made, no clue how *that* would draw
export const TerminatorsVI = ( export const TerminatorsVI = (
@ -10,12 +13,57 @@ export const TerminatorsVI = (
previousFix: NavFix, previousFix: NavFix,
lastCourse: number lastCourse: number
): [NavFix?, LineSegment[]?] => { ): [NavFix?, LineSegment[]?] => {
const [crs, nextFix] = getCourseAndFixForIntercepts(nextLeg, previousFix);
const speed = computeSpeed(leg, previousFix); const speed = computeSpeed(leg, previousFix);
const crsIntoEndpoint = leg.Course.toTrue(previousFix);
const [crsToIntercept, nextFix] = getCourseAndFixForIntercepts(nextLeg, previousFix);
const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]];
// Compute overfly arc
if (previousFix.isFlyOver && !lastCourse.equal(crsIntoEndpoint)) {
const turnRate = computeTurnRate(speed, Parser.AC_BANK);
const updatedCrsToIntercept = getGreatCircleBearing(previousFix, nextFix);
// 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
while (!lastCourse.equal(crsIntoEndpoint) && !updatedCrsToIntercept.equal(crsToIntercept)) {
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 = computeDestinationPoint(
{
latitude: line.at(-1)![1],
longitude: line.at(-1)![0],
},
((speed / 3600) * time).toMetre(),
lastCourse
);
line.push([arcFix.longitude, arcFix.latitude]);
// Update previousFix
previousFix.latitude = arcFix.latitude;
previousFix.longitude = arcFix.longitude;
}
}
// Compute INTC
const interceptFix: NavFix = { const interceptFix: NavFix = {
...computeIntersection(previousFix, leg.Course.toTrue(nextFix), nextFix, crs)!, ...computeIntersection(previousFix, leg.Course.toTrue(nextFix), nextFix, crsToIntercept)!,
isFlyOver: leg.IsFlyOver, isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude, altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: speed, speed: speed,
@ -23,28 +71,7 @@ export const TerminatorsVI = (
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
}; };
const line = handleTurnAtFix( line.push([interceptFix.longitude, interceptFix.latitude]);
crs,
leg.Course.toTrue(nextFix),
lastCourse,
previousFix,
interceptFix,
speed,
leg.TurnDir
);
// Intercept based on previous intercept
const interceptPoint2 = computeIntersection(
{ latitude: line.at(-2)![1], longitude: line.at(-2)![0] },
leg.Course.toTrue(nextFix),
nextFix,
crs
);
if (interceptPoint2)
return [
{ ...interceptFix, ...interceptPoint2 },
[...line.slice(0, -1), [interceptPoint2.longitude, interceptPoint2.latitude]],
];
return [interceptFix, line]; return [interceptFix, line];
}; };

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import { handleTurnAtFix } from '../pathGenerators/handleTurnAtFix'; import { handleTurnAtFix } from '../pathGenerators/handleTurnAtFix';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -10,7 +10,7 @@ export const TerminatorsVM = (
): [NavFix?, LineSegment[]?] => { ): [NavFix?, LineSegment[]?] => {
const speed = computeSpeed(leg, previousFix); const speed = computeSpeed(leg, previousFix);
const endpoint = geolib.computeDestinationPoint(previousFix, (10).toMetre(), leg.Course.toTrue(previousFix)); const endpoint = computeDestinationPoint(previousFix, (10).toMetre(), leg.Course.toTrue(previousFix));
const line = handleTurnAtFix( const line = handleTurnAtFix(
leg.Course.toTrue(previousFix), leg.Course.toTrue(previousFix),

View File

@ -1,4 +1,4 @@
import * as geolib from 'geolib'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
/** /**
* @param leg Leg to examine * @param leg Leg to examine
@ -18,9 +18,9 @@ export const getCourseAndFixForIntercepts = (leg: TerminalEntry, origin: NavFix)
return [_leg.Course.toTrue(fix), fix]; return [_leg.Course.toTrue(fix), fix];
} }
case 'TF': { case 'TF': {
const _leg = leg as FMTerminalEntry; const _leg = leg as TFTerminalEntry;
return [ return [
geolib.getGreatCircleBearing(origin, { getGreatCircleBearing(origin, {
latitude: _leg.WptLat, latitude: _leg.WptLat,
longitude: _leg.WptLon, longitude: _leg.WptLon,
}), }),
@ -33,10 +33,15 @@ export const getCourseAndFixForIntercepts = (leg: TerminalEntry, origin: NavFix)
return [_leg.Course.reciprocalCourse().toTrue(fix), fix]; return [_leg.Course.reciprocalCourse().toTrue(fix), fix];
} }
case 'DF': { case 'DF': {
const _leg = leg as FMTerminalEntry; const _leg = leg as DFTerminalEntry;
const fix = { latitude: _leg.WptLat, longitude: _leg.WptLon }; const fix = { latitude: _leg.WptLat, longitude: _leg.WptLon };
return [-1, fix]; return [-1, fix];
} }
case 'RF': {
const _leg = leg as RFTerminalEntry;
const fix = { latitude: _leg.WptLat, longitude: _leg.WptLon };
return [_leg.Course.toTrue(fix), fix];
}
default: { default: {
return [-1, origin]; return [-1, origin];
} }

9
browser/src/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
export declare global {
type Chart = {
data: string;
index_number: string;
bounds: LatLngBoundsExpression;
};
type Procedure = { name: string; data: object };
}