Compare commits

..

No commits in common. "c5cd3c7a0ed7527b5e1a4c0f487f4fb0c294f675" and "e4adf306321736ea4171ff66f598776432688513" have entirely different histories.

31 changed files with 329 additions and 746 deletions

View File

@ -10,7 +10,7 @@
### Example ### Example
LGAV 03L BIBE1K SID (Cycle 2507, ID 10653) LGAV BIBE1K SID (Cycle 2507, ID 10653)
### Minimum Required Fields ### Minimum Required Fields
@ -40,14 +40,12 @@ LGAV 03L BIBE1K SID (Cycle 2507, ID 10653)
While similar to an RF, the center point is coded differently. While similar to an RF, the center point is coded differently.
Calculate distance for FMS based on ` 2 * π NavDist * abs(NavBear - Course) 360`.
## Course to Altitude (CA) ## Course to Altitude (CA)
### Example ### Example
LGAV 03L BIBE1L SID (Cycle 2507, ID 10654) LGAV BIBE1L SID (Cycle 2507, ID 10654)
### Minimum Required Fields ### Minimum Required Fields
@ -76,7 +74,7 @@ This new origin is an implicit overfly.
### Example ### Example
LGAV 21L BIBE2F SID (Cycle 2507, ID 10657) LGAV BIBE2F SID (Cycle 2507, ID 10657)
### Minimum Required Fields ### Minimum Required Fields
@ -100,13 +98,14 @@ LGAV 21L BIBE2F SID (Cycle 2507, ID 10657)
The leg terminates upon reaching `Distance`. The leg terminates upon reaching `Distance`.
This intercept point then becomes the origin fix of the succeeding leg. This intercept point then becomes the origin fix of the succeeding leg.
This new origin is an implicit overfly.
## Course to Fix (CF) ## Course to Fix (CF)
### Example ### Example
LGAV 21L BIBE2F SID (Cycle 2507, ID 10657) LGAV BIBE2F SID (Cycle 2507, ID 10657)
### Minimum Required Fields ### Minimum Required Fields
@ -146,7 +145,7 @@ shall be `TurnDir`.
### Example ### Example
LGAV 03L BIBE1L SID (Cycle 2507, ID 10654) LGAV BIBE1L SID (Cycle 2507, ID 10654)
### Minimum Required Fields ### Minimum Required Fields
@ -171,7 +170,7 @@ This new origin can never be an overfly due to the intercept nature.
### Example ### Example
LGAV 03L KOR1D SID (Cycle 2507, ID 10679) LGAV KOR1D SID (Cycle 2507, ID 10679)
### Minimum Required Fields ### Minimum Required Fields
@ -198,7 +197,7 @@ This intercept point then becomes the origin fix of the succeeding leg.
### Example ### Example
LGAV 03L KOR1D SID (Cycle 2507, ID 10679) LGAV KOR1D SID (Cycle 2507, ID 10679)
### Minimum Required Fields ### Minimum Required Fields
@ -220,7 +219,7 @@ LGAV 03L KOR1D SID (Cycle 2507, ID 10679)
### Example ### Example
LGAV 21L BIBE2F SID (Cycle 2507, ID 10657) LGAV BIBE2F SID (Cycle 2507, ID 10657)
### Minimum Required Fields ### Minimum Required Fields
@ -255,7 +254,7 @@ This new origin is an implicit overfly.
### Example ### Example
LIED 34L/R CAR6F SID (Cycle 2507, ID 11798) LIED CAR6F SID (Cycle 2507, ID 11798)
### Minimum Required Fields ### Minimum Required Fields
@ -291,7 +290,7 @@ This intercept point then becomes the origin fix of the succeeding leg.
### Example ### Example
LGAV 03R BIBE2T SID (Cycle 2507, ID 10659) LGAV BIBE2T SID (Cycle 2507, ID 10659)
### Minimum Required Fields ### Minimum Required Fields
@ -319,13 +318,14 @@ LGAV 03R BIBE2T SID (Cycle 2507, ID 10659)
The leg terminates upon reaching `Distance`. The leg terminates upon reaching `Distance`.
This intercept point then becomes the origin fix of the succeeding leg. This intercept point then becomes the origin fix of the succeeding leg.
This new origin is an implicit overfly.
## Track from Fix to Manual Termination (FM) ## Track from Fix to Manual Termination (FM)
### Example ### Example
LFPV 27 PB2V SID (Cycle 2507, ID 10395) LFPV PB2V SID (Cycle 2507, ID 10395)
### Minimum Required Fields ### Minimum Required Fields
@ -457,7 +457,7 @@ My guess as for the missing time/distance decider field, assume it to be distanc
### Example ### Example
FAWB 29 VDM29 APP (Cycle 2507, ID 67794), Missed approach procedure FAWB VDM29 APP (Cycle 2507, ID 67794), Missed approach procedure
### Minimum Required Fields ### Minimum Required Fields
@ -513,7 +513,7 @@ This intercept point then becomes the origin fix of the succeeding leg.
### Example ### Example
LFRN 10 GODA5R SID (cycle 2507, ID 10485) LFRN GODA5R SID (cycle 2507, ID 10485)
### Minimum Required Fields ### Minimum Required Fields
@ -545,14 +545,12 @@ No radius is specified, but can be inferred based on center point, both endpoint
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.
Use `Distance` for calculations in FMS.
## Track to Fix (TF) ## Track to Fix (TF)
### Example ### Example
LFRN 10 GODA5R SID (cycle 2507, ID 10485) LFRN GODA5R SID (cycle 2507, ID 10485)
### Minimum Required Fields ### Minimum Required Fields
@ -573,7 +571,7 @@ LFRN 10 GODA5R SID (cycle 2507, ID 10485)
### Example ### Example
LFRK 10 LGL4X SID (Cycle 2507, ID 10475) LFRK LGL4X SID (Cycle 2507, ID 10475)
### Minimum Required Fields ### Minimum Required Fields
@ -601,7 +599,7 @@ This new origin is an implicit overfly.
### Example ### Example
LFRK 31 NEVI4Y SID (Cycle 2507, ID 10482) LFRK NEVI4Y SID (Cycle 2507, ID 10482)
### Minimum Required Fields ### Minimum Required Fields
@ -624,13 +622,14 @@ LFRK 31 NEVI4Y SID (Cycle 2507, ID 10482)
The leg terminates upon reaching `Distance`. The leg terminates upon reaching `Distance`.
This intercept point then becomes the origin fix of the succeeding leg. This intercept point then becomes the origin fix of the succeeding leg.
This new origin is an implicit overfly.
## Heading to Intercept (VI) ## Heading to Intercept (VI)
### Example ### Example
LFRK 31 LUSI4Y SID (Cycle 2507, ID 10480) LFRK LUSI4Y SID (Cycle 2507, ID 10480)
### Minimum Required Fields ### Minimum Required Fields
@ -657,7 +656,7 @@ This new origin can never be an overfly due to the intercept nature.
### Example ### Example
LFPV 27 PB2P SID (Cycle 2507, ID 10394) LFPV PB2P SID (Cycle 2507, ID 10394)
### Minimum Required Fields ### Minimum Required Fields
@ -677,7 +676,7 @@ LFPV 27 PB2P SID (Cycle 2507, ID 10394)
### Example ### Example
LIMC 35R MMP8G SID (Cycle 2507, ID 11909) LIMC MMP8G SID (Cycle 2507, ID 11909)
### Minimum Required Fields ### Minimum Required Fields
@ -698,8 +697,3 @@ LIMC 35R MMP8G SID (Cycle 2507, ID 11909)
The leg terminates at the intercept point. The leg terminates at the intercept point.
This intercept point then becomes the origin fix of the succeeding leg. This intercept point then becomes the origin fix of the succeeding leg.
# Other Fields
- `Vnav`: Angle from IAF to MAP. Useful for RNAV.

View File

@ -24,7 +24,7 @@ function App() {
return ( return (
<> <>
{!user ? ( {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>}
@ -38,27 +38,7 @@ function App() {
</a> </a>
</> </>
)} )}
</div> {user && (
) : (
<div className="flex h-dvh w-dvw">
{selectedAirport && selectedRunway && selectedTerminal ? (
<Sidebar
airport={selectedAirport}
runway={selectedRunway}
terminal={selectedTerminal}
transitions={transitions}
transition={selectedTransition}
chart={selectedChart}
setTransition={setSelectedTransition}
setChart={setSelectedChart}
backAction={() => {
setSelectedTerminal(undefined);
setSelectedChart(undefined);
setTransitions([]);
setSelectedTransition(undefined);
}}
/>
) : (
<ProcedureSelect <ProcedureSelect
selectedAirport={selectedAirport} selectedAirport={selectedAirport}
selectedRunway={selectedRunway} selectedRunway={selectedRunway}
@ -67,29 +47,39 @@ function App() {
setSelectedRunway={setSelectedRunway} setSelectedRunway={setSelectedRunway}
setSelectedTerminal={setSelectedTerminal} setSelectedTerminal={setSelectedTerminal}
handleSelection={(selectedTransitions) => { handleSelection={(selectedTransitions) => {
let _transitions = selectedTransitions const _transitions = selectedTransitions.map((transition) => ({
.map((transition) => ({
name: transition, name: transition,
data: parser.parse(selectedRunway!, transition), data: parser.parse(selectedRunway!, transition),
})) }));
.filter(
(transition) =>
!transition.name.startsWith('RW') || transition.name === `RW${selectedRunway?.Ident}`
);
if (_transitions.length > 1)
_transitions = _transitions.filter((transition) => transition.name !== 'ALL');
setTransitions(_transitions); setTransitions(_transitions);
setSelectedTransition(_transitions[0]); setSelectedTransition(_transitions[0]);
setSelectedChart(undefined); setSelectedChart(undefined);
}} }}
/> />
)} )}
<Map </div>
) : (
<div className="flex h-dvh w-dvw">
{transitions.length > 0 && selectedAirport && selectedTerminal ? (
<>
<Sidebar
airport={selectedAirport} airport={selectedAirport}
terminal={selectedTerminal}
transitions={transitions}
transition={selectedTransition}
chart={selectedChart} chart={selectedChart}
procedures={selectedTransition ? [selectedTransition] : []} setTransition={setSelectedTransition}
setChart={setSelectedChart}
backAction={() => {
setSelectedTerminal(undefined);
setTransitions([]);
}}
/> />
<Map airport={selectedAirport} chart={selectedChart} procedure={selectedTransition} />
</>
) : (
<h1 className="text-center text-3xl">Error</h1>
)}
</div> </div>
)} )}
</> </>

View File

@ -1,22 +0,0 @@
import type { FC } from 'react';
interface LoaderProps {
size?: 0.5 | 1 | 2 | 3;
}
export const Loader: FC<LoaderProps> = ({ size = 2 }) => {
const variants = {
0.5: 'size-6 border-1 after:top-1 after:left-0.5 after:size-1.5 after:border-1',
1: 'size-12 border-2 after:top-2 after:left-1 after:size-3 after:border-2',
2: 'size-24 border-4 after:top-3 after:left-2 after:size-6 after:border-4',
3: 'size-36 border-6 after:top-4 after:left-3 after:size-9 after:border-6',
};
return (
<div className="flex justify-center">
<span
className={`relative inline-block ${variants[size]} animate-spin rounded-[50%] border-red-500 after:absolute after:rounded-[50%] after:border-black after:content-['']`}
></span>
</div>
);
};

View File

@ -1,26 +1,22 @@
import { default as L } from 'leaflet'; import { default as L } from 'leaflet';
import 'leaflet-svg-shape-markers'; import 'leaflet-svg-shape-markers';
import { createRef, Fragment, useEffect, type FC } from 'react'; import { createRef, type FC } from 'react';
import { GeoJSON, ImageOverlay, MapContainer, TileLayer } from 'react-leaflet'; import { GeoJSON, ImageOverlay, MapContainer, TileLayer } from 'react-leaflet';
interface MapProps { interface MapProps {
airport: Airport | undefined; airport: Airport;
chart: Chart | undefined; chart: Chart | undefined;
procedures: Procedure[]; procedure: Procedure | undefined;
} }
export const Map: FC<MapProps> = ({ airport, chart, procedures }) => { export const Map: FC<MapProps> = ({ airport, chart, procedure }) => {
const mapRef = createRef<L.Map>(); const mapRef = createRef<L.Map>();
const imageRef = createRef<L.ImageOverlay>(); const imageRef = createRef<L.ImageOverlay>();
useEffect(() => {
if (airport) mapRef.current?.flyTo([airport?.Latitude, airport?.Longitude], 10, { animate: false });
}, [airport]);
return ( return (
<MapContainer <MapContainer
center={[0, 0]} center={[airport.Latitude, airport.Longitude]}
zoom={5} zoom={13}
zoomSnap={0} zoomSnap={0}
className="h-full w-full" className="h-full w-full"
ref={(_mapRef) => { ref={(_mapRef) => {
@ -50,15 +46,15 @@ export const Map: FC<MapProps> = ({ airport, chart, procedures }) => {
/> />
)} )}
{procedures.map((procedure) => ( {procedure && (
<Fragment key={procedure.name}> <>
<GeoJSON <GeoJSON
key={`${procedure.name}-lines`} key={`${procedure.name}-lines`}
data={procedure.data} data={procedure.data}
style={({ properties }) => ({ style={({ properties }) => ({
color: properties.isMAP ? '#00ffff' : '#ff00ff', color: '#ff00ff',
stroke: true, stroke: true,
weight: properties.isMAP ? 2.5 : 5, weight: 5,
opacity: 1, opacity: 1,
dashArray: properties.isManual ? '20, 20' : undefined, dashArray: properties.isManual ? '20, 20' : undefined,
})} })}
@ -75,22 +71,12 @@ export const Map: FC<MapProps> = ({ airport, chart, procedures }) => {
weight: 3, weight: 3,
}} }}
pointToLayer={({ properties }, latlng) => { pointToLayer={({ properties }, latlng) => {
if (properties.isIntersection) return L.circleMarker(latlng, { radius: 6 }); if (properties.isFlyOver)
else if (properties.IsMAP)
return L.shapeMarker(latlng, {
shape: 'square',
radius: 6,
});
else if (properties.IsFAF)
return L.shapeMarker(latlng, {
shape: 'diamond',
radius: 6,
});
else if (properties.isFlyOver)
return L.shapeMarker(latlng, { return L.shapeMarker(latlng, {
shape: 'triangle', shape: 'triangle',
radius: 6, radius: 6,
}); });
if (properties.isIntersection) return L.circleMarker(latlng, { radius: 6 });
return L.shapeMarker(latlng, { return L.shapeMarker(latlng, {
shape: 'star-4', shape: 'star-4',
@ -112,8 +98,8 @@ export const Map: FC<MapProps> = ({ airport, chart, procedures }) => {
}} }}
filter={(feature) => feature.geometry.type === 'Point'} filter={(feature) => feature.geometry.type === 'Point'}
/> />
</Fragment> </>
))} )}
</MapContainer> </MapContainer>
); );
}; };

View File

@ -1,6 +1,5 @@
import { createRef, useMemo, useState, type Dispatch, type FC, type SetStateAction } from 'react'; import { createRef, useMemo, useState, type Dispatch, type FC, type SetStateAction } from 'react';
import Parser from '../parser/parser'; import Parser from '../parser/parser';
import { Loader } from './Loader';
const parser = await Parser.instance(); const parser = await Parser.instance();
@ -25,32 +24,26 @@ export const ProcedureSelect: FC<ProcedureSelectProps> = ({
}) => { }) => {
const inputRef = createRef<HTMLInputElement>(); const inputRef = createRef<HTMLInputElement>();
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [inFlight, setInFlight] = useState<number>();
const runways = useMemo( const runways = useMemo(
() => parser.runways.filter(({ AirportID }) => AirportID === selectedAirport?.ID), () => parser.runways.filter(({ AirportID }) => AirportID === selectedAirport?.ID),
[selectedAirport] [selectedAirport]
); );
const terminals = useMemo(() => { const terminals = useMemo(
const _terminals = parser.terminals.filter( () =>
parser.terminals.filter(
({ AirportID, RwyID }) => AirportID === selectedAirport?.ID && (!RwyID || RwyID === selectedRunway?.ID) ({ AirportID, RwyID }) => AirportID === selectedAirport?.ID && (!RwyID || RwyID === selectedRunway?.ID)
),
[selectedAirport, selectedRunway]
); );
return {
star: _terminals.filter((terminal) => terminal.Proc === 1),
sid: _terminals.filter((terminal) => terminal.Proc === 2),
iap: _terminals.filter((terminal) => terminal.Proc === 3),
};
}, [selectedAirport, selectedRunway]);
return ( return (
<div className="flex h-full w-[300px] shrink-0 flex-col overflow-hidden"> <div className="flex w-full flex-col items-center justify-center gap-2 p-2">
{selectedAirport && ( {selectedAirport && (
<button <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" className="fixed top-2 left-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={() => { onClick={() => {
setError(undefined); setError(undefined);
setInFlight(undefined);
if (selectedTerminal) { if (selectedTerminal) {
setSelectedTerminal(undefined); setSelectedTerminal(undefined);
@ -65,7 +58,6 @@ export const ProcedureSelect: FC<ProcedureSelectProps> = ({
</button> </button>
)} )}
<div className="flex h-[calc(100%-50px)] flex-col gap-2 overflow-y-auto px-2 pb-2">
{!selectedAirport && ( {!selectedAirport && (
<div className="flex w-full flex-col gap-2"> <div className="flex w-full flex-col gap-2">
<h1 className="text-center text-3xl">Enter Airport ICAO</h1> <h1 className="text-center text-3xl">Enter Airport ICAO</h1>
@ -94,7 +86,6 @@ export const ProcedureSelect: FC<ProcedureSelectProps> = ({
{error && <span className="text-center text-red-700">{error}</span>} {error && <span className="text-center text-red-700">{error}</span>}
</div> </div>
)} )}
{selectedAirport && !selectedRunway && ( {selectedAirport && !selectedRunway && (
<div className="flex w-full flex-col gap-2"> <div className="flex w-full flex-col gap-2">
<h1 className="text-center text-3xl">Select Runway</h1> <h1 className="text-center text-3xl">Select Runway</h1>
@ -109,41 +100,13 @@ export const ProcedureSelect: FC<ProcedureSelectProps> = ({
))} ))}
</div> </div>
)} )}
{selectedAirport && selectedRunway && !selectedTerminal && ( {selectedAirport && selectedRunway && !selectedTerminal && (
<div className="flex w-full flex-col gap-2"> <div className="flex w-full flex-col gap-2">
<h1 className="text-center text-3xl">Select Procedure</h1> <h1 className="text-center text-3xl">Select Procedure</h1>
{terminals.map((terminal) => (
<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">Arrivals</div>
{terminals.star.map((terminal) => (
<button <button
key={terminal.ID} key={terminal.ID}
className="cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus-visible:outline-2 disabled:bg-gray-100" className="cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus-visible:outline-2"
onClick={() => {
setInFlight(terminal.ID);
parser.loadTerminal(terminal.ID).then(() => {
setSelectedTerminal(terminal);
const transitions = new Set(parser.procedures.map((proc) => proc.Transition));
handleSelection(Array.from(transitions));
setInFlight(undefined);
});
}}
disabled={!!inFlight}
>
<div className="flex justify-between">
{terminal.FullName}
{inFlight === terminal.ID && <Loader size={0.5} />}
</div>
</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">Departures</div>
{terminals.sid.map((terminal) => (
<button
key={terminal.ID}
className="cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus-visible:outline-2 disabled:bg-gray-100"
onClick={() => { onClick={() => {
parser.loadTerminal(terminal.ID).then(() => { parser.loadTerminal(terminal.ID).then(() => {
setSelectedTerminal(terminal); setSelectedTerminal(terminal);
@ -151,40 +114,12 @@ export const ProcedureSelect: FC<ProcedureSelectProps> = ({
handleSelection(Array.from(transitions)); handleSelection(Array.from(transitions));
}); });
}} }}
disabled={!!inFlight}
> >
<div className="flex justify-between">
{terminal.FullName} {terminal.FullName}
{inFlight === terminal.ID && <Loader size={0.5} />}
</div>
</button> </button>
))} ))}
</div> </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">Approaches</div>
{terminals.iap.map((terminal) => (
<button
key={terminal.ID}
className="cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus-visible:outline-2 disabled:bg-gray-100"
onClick={() => {
parser.loadTerminal(terminal.ID).then(() => {
setSelectedTerminal(terminal);
const transitions = new Set(parser.procedures.map((proc) => proc.Transition));
handleSelection(Array.from(transitions));
});
}}
disabled={!!inFlight}
>
<div className="flex justify-between">
{terminal.FullName}
{inFlight === terminal.ID && <Loader size={0.5} />}
</div>
</button>
))}
</div>
</div>
)} )}
</div> </div>
</div>
); );
}; };

View File

@ -1,13 +1,11 @@
import BrowserImageManipulation from 'browser-image-manipulation'; import BrowserImageManipulation from 'browser-image-manipulation';
import L from 'leaflet'; import L from 'leaflet';
import type { Chart as NGChart } from 'navigraph/charts'; import type { Chart as NGChart } from 'navigraph/charts';
import { useCallback, useEffect, useState, type Dispatch, type FC, type SetStateAction } from 'react'; import { useEffect, useState, type Dispatch, type FC, type SetStateAction } from 'react';
import { charts } from '../lib/navigraph'; import { charts } from '../lib/navigraph';
import { Loader } from './Loader';
interface SidebarProps { interface SidebarProps {
airport: Airport; airport: Airport;
runway: Runway;
terminal: Terminal; terminal: Terminal;
transitions: Procedure[]; transitions: Procedure[];
transition: Procedure | undefined; transition: Procedure | undefined;
@ -19,7 +17,6 @@ interface SidebarProps {
export const Sidebar: FC<SidebarProps> = ({ export const Sidebar: FC<SidebarProps> = ({
airport, airport,
runway,
terminal, terminal,
transitions, transitions,
transition, transition,
@ -28,83 +25,14 @@ export const Sidebar: FC<SidebarProps> = ({
setChart, setChart,
backAction, backAction,
}) => { }) => {
const [inFlight, setInFlight] = useState(false); const [chartIndex, setChartIndex] = useState<NGChart[]>([]);
const [chartIndex, setChartIndex] = useState<{
sid: NGChart[];
star: NGChart[];
iap: NGChart[];
}>({ star: [], sid: [], iap: [] });
useEffect(() => { useEffect(() => {
setInFlight(true);
(async () => { (async () => {
const _chartIndex = await charts.getChartsIndex({ icao: airport.ICAO, version: 'STD' }); setChartIndex((await charts.getChartsIndex({ icao: airport.ICAO, version: 'STD' })) ?? []);
const newChartIndex = {
star:
_chartIndex?.filter(
(_chart) =>
_chart.category === 'ARR' && (_chart.runways.length === 0 || _chart.runways.includes(runway.Ident))
) ?? [],
sid:
_chartIndex?.filter(
(_chart) =>
_chart.category === 'DEP' && (_chart.runways.length === 0 || _chart.runways.includes(runway.Ident))
) ?? [],
iap:
_chartIndex?.filter(
(_chart) =>
_chart.category === 'APP' && (_chart.runways.length === 0 || _chart.runways.includes(runway.Ident))
) ?? [],
};
_chartIndex?.filter((_chart) => ['ARR', 'DEP', 'APP'].includes(_chart.category));
setChartIndex(newChartIndex);
setInFlight(false);
})(); })();
}, [airport.ICAO]); }, [airport.ICAO]);
useEffect(() => {
if (terminal) {
const star = chartIndex.star.find((_chart) => _chart.procedures.includes(terminal.FullName));
const sid = chartIndex.sid.find((_chart) => _chart.procedures.includes(terminal.FullName));
const iap = chartIndex.iap.find((_chart) => _chart.procedures.includes(terminal.FullName));
if (star) findChart(star);
if (sid) findChart(sid);
if (iap) findChart(iap);
}
}, [terminal, chartIndex]);
const findChart = useCallback(
async (_chart: NGChart) => {
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 });
},
[chart]
);
return ( return (
<div className="flex h-full w-[300px] shrink-0 flex-col overflow-hidden"> <div className="flex h-full w-[300px] shrink-0 flex-col overflow-hidden">
<button <button
@ -129,24 +57,45 @@ export const Sidebar: FC<SidebarProps> = ({
setTransition(_procedure); setTransition(_procedure);
}} }}
> >
{_procedure.name ? _procedure.name : 'NONE'} {_procedure.name ? _procedure.name : 'ZZZZ'}
</button> </button>
))} ))}
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="sticky top-0 z-10 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">Charts</div> <div className="sticky top-0 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">Charts</div>
{inFlight && <Loader size={3} />} {chartIndex
<div className="flex flex-col gap-2">
<div className="sticky top-7 -mx-2 -mt-2 bg-gray-500 px-2 text-lg font-semibold text-white">Arrivals</div>
{chartIndex.star
.filter((_chart) => _chart.is_georeferenced) .filter((_chart) => _chart.is_georeferenced)
.map((_chart) => ( .map((_chart) => (
<button <button
key={_chart.index_number} 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' : ''}`} 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={() => findChart(_chart)} 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> <span className="font-bold">{_chart.index_number}</span>
<br /> <br />
@ -154,41 +103,6 @@ export const Sidebar: FC<SidebarProps> = ({
</button> </button>
))} ))}
</div> </div>
<div className="flex flex-col gap-2">
<div className="sticky top-7 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">Departures</div>
{chartIndex.sid
.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={() => findChart(_chart)}
>
<span className="font-bold">{_chart.index_number}</span>
<br />
{_chart.name}
</button>
))}
</div>
<div className="flex flex-col gap-2">
<div className="sticky top-7 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">Approaches</div>
{chartIndex.iap
.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={() => findChart(_chart)}
>
<span className="font-bold">{_chart.index_number}</span>
<br />
{_chart.name}
</button>
))}
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -128,32 +128,28 @@ class Parser {
name: runway.Ident, name: runway.Ident,
}); });
let lastCourse = runway.TrueHeading; let lastCourse = runway.TrueHeading;
const legOptions = { isMAP: false }; const procedure = this._procedures.filter(({ Transition }) => !Transition || Transition === transition);
const procedure = this._procedures.filter(
({ Transition }) => !Transition || Transition === transition || Transition === 'ALL'
);
// Main // Main
for (let index = 0; index < procedure.length; index++) { for (let index = 0; index < procedure.length; index++) {
const leg = procedure[index]; const leg = procedure[index];
const previousFix = navFixes.at(-1)!; const previousFix = navFixes.at(-1)!;
legOptions.isMAP ||= previousFix.IsMAP ?? false; const waypoint = this.waypoints.filter(({ ID }) => ID === leg.WptID)[0];
const waypoint = this.waypoints.find(({ ID }) => ID === leg.WptID);
switch (leg.TrackCode) { switch (leg.TrackCode) {
case 'AF': { case 'AF': {
const [fixToAdd, lineToAdd] = TerminatorsAF(leg as AFTerminalEntry, previousFix, waypoint); const [fixToAdd, lineToAdd] = TerminatorsAF(leg as AFTerminalEntry, previousFix, waypoint);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'CA': { case 'CA': {
const [fixToAdd, lineToAdd] = TerminatorsCA(leg as CATerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsCA(leg as CATerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'CD': { case 'CD': {
const [fixToAdd, lineToAdd] = TerminatorsCD(leg as CDTerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsCD(leg as CDTerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'CF': { case 'CF': {
@ -163,7 +159,7 @@ class Parser {
lastCourse, lastCourse,
waypoint waypoint
); );
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'CI': { case 'CI': {
@ -173,39 +169,37 @@ class Parser {
{ ...previousFix }, // COPY { ...previousFix }, // COPY
lastCourse lastCourse
); );
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'CR': { case 'CR': {
const [fixToAdd, lineToAdd] = TerminatorsCR(leg as CRTerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsCR(leg as CRTerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'DF': { case 'DF': {
const [fixToAdd, lineToAdd] = TerminatorsDF(leg as DFTerminalEntry, previousFix, lastCourse, waypoint); const [fixToAdd, lineToAdd] = TerminatorsDF(leg as DFTerminalEntry, previousFix, lastCourse, waypoint);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'FA': { case 'FA': {
const [fixToAdd, lineToAdd] = TerminatorsFA(leg as FATerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsFA(leg as FATerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'FC': { case 'FC': {
const [fixToAdd, lineToAdd] = TerminatorsFC(leg as FCTerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsFC(leg as FCTerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'FD': { case 'FD': {
const [fixToAdd, lineToAdd] = TerminatorsFD(leg as FDTerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsFD(leg as FDTerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'FM': { case 'FM': {
const [fixToAdd, lineToAdd] = TerminatorsFM(leg as FMTerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsFM(leg as FMTerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, { isManual: true, ...legOptions }); update(fixToAdd, lineToAdd, { isManual: true });
// Make overfly
navFixes.at(-1)!.isFlyOver = true;
break; break;
} }
case 'HA': case 'HA':
@ -215,11 +209,8 @@ class Parser {
break; break;
case 'IF': { case 'IF': {
const fixToAdd = TerminatorsIF(leg as RFTerminalEntry, waypoint); const fixToAdd = TerminatorsIF(leg as RFTerminalEntry, waypoint);
// Only Runway, replace navFixes.length = 0;
if (navFixes.length <= 1) {
navFixes.push(fixToAdd); navFixes.push(fixToAdd);
lastCourse = -1;
}
break; break;
} }
case 'PI': case 'PI':
@ -232,28 +223,23 @@ class Parser {
lastCourse = (leg as RFTerminalEntry).Course?.toTrue(fixToAdd); lastCourse = (leg as RFTerminalEntry).Course?.toTrue(fixToAdd);
} }
if (lineToAdd) { if (lineToAdd) {
lineSegments.push({ line: lineToAdd, ...legOptions }); lineSegments.push({ line: lineToAdd });
} }
break; break;
} }
case 'TF': { case 'TF': {
const [fixToAdd, lineToAdd] = TerminatorsTF( const [fixToAdd, lineToAdd] = TerminatorsTF(leg as TFTerminalEntry, previousFix, lastCourse, waypoint);
leg as TFTerminalEntry, update(fixToAdd, lineToAdd);
{ ...previousFix }, // COPY
lastCourse,
waypoint
);
update(fixToAdd, lineToAdd, legOptions);
break; break;
} }
case 'VA': { case 'VA': {
const [fixToAdd, lineToAdd] = TerminatorsVA(leg as VATerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsVA(leg as VATerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'VD': { case 'VD': {
const [fixToAdd, lineToAdd] = TerminatorsVD(leg as VDTerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsVD(leg as VDTerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'VI': { case 'VI': {
@ -263,19 +249,17 @@ class Parser {
{ ...previousFix }, // COPY { ...previousFix }, // COPY
lastCourse lastCourse
); );
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
case 'VM': { case 'VM': {
const [fixToAdd, lineToAdd] = TerminatorsVM(leg as VMTerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsVM(leg as VMTerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, { isManual: true, ...legOptions }); update(fixToAdd, lineToAdd, { isManual: true });
// Make overfly
navFixes.at(-1)!.isFlyOver = true;
break; break;
} }
case 'VR': { case 'VR': {
const [fixToAdd, lineToAdd] = TerminatorsVR(leg as VRTerminalEntry, previousFix, lastCourse); const [fixToAdd, lineToAdd] = TerminatorsVR(leg as VRTerminalEntry, previousFix, lastCourse);
update(fixToAdd, lineToAdd, legOptions); update(fixToAdd, lineToAdd);
break; break;
} }
default: default:

View File

@ -6,7 +6,7 @@ import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
* @param start Arc origin point * @param start Arc origin point
* @param center Arc center point * @param center Arc center point
* @param radius Arc radius in nmi * @param radius Arc radius in nmi
* @param turnDir Turn direction * @param turnDir
* @returns Line segments * @returns Line segments
*/ */
export const generateAFArc = ( export const generateAFArc = (
@ -30,11 +30,11 @@ export const generateAFArc = (
while (crsFromOrigin !== crsIntoEndpoint) { while (crsFromOrigin !== crsIntoEndpoint) {
if (turnDir === 'R') { if (turnDir === 'R') {
const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees(); const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees();
crsFromOrigin += delta < 0.1 ? delta : 0.1; crsFromOrigin += delta < 1 ? delta : 1;
crsFromOrigin = crsFromOrigin.normaliseDegrees(); crsFromOrigin = crsFromOrigin.normaliseDegrees();
} else { } else {
const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees(); const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees();
crsFromOrigin -= delta < 0.1 ? delta : 0.1; crsFromOrigin -= delta < 1 ? delta : 1;
crsFromOrigin = crsFromOrigin.normaliseDegrees(); crsFromOrigin = crsFromOrigin.normaliseDegrees();
} }
if (crsFromOrigin === crsIntoEndpoint) break; if (crsFromOrigin === crsIntoEndpoint) break;

View File

@ -38,12 +38,12 @@ export const generatePerformanceArc = (
let time = 0; let time = 0;
if (turnDir === 'R') { if (turnDir === 'R') {
const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees(); const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
crsFromOrigin = (crsFromOrigin + increment).normaliseDegrees(); crsFromOrigin = (crsFromOrigin + increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} else { } else {
const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees(); const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
crsFromOrigin = (crsFromOrigin - increment).normaliseDegrees(); crsFromOrigin = (crsFromOrigin - increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} }
@ -70,12 +70,12 @@ export const generatePerformanceArc = (
let time = 0; let time = 0;
if (turnDir === 'R') { if (turnDir === 'R') {
const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees(); const delta = (crsIntoEndpoint - crsFromOrigin).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
crsFromOrigin = (crsFromOrigin + increment).normaliseDegrees(); crsFromOrigin = (crsFromOrigin + increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} else { } else {
const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees(); const delta = (crsFromOrigin - crsIntoEndpoint).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
crsFromOrigin = (crsFromOrigin - increment).normaliseDegrees(); crsFromOrigin = (crsFromOrigin - increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} }

View File

@ -1,5 +1,5 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getPreciseDistance from 'geolib/es/getPreciseDistance'; import getDistance from 'geolib/es/getDistance';
/** /**
* @param crsIntoEndpoint Course into arc endpoint * @param crsIntoEndpoint Course into arc endpoint
@ -36,7 +36,7 @@ export const generateRFArc = (
crsOrthogonalOnEndpoint = (crsIntoEndpoint - 90).normaliseDegrees(); crsOrthogonalOnEndpoint = (crsIntoEndpoint - 90).normaliseDegrees();
} }
const arcRad = getPreciseDistance(center, start); const arcRad = getDistance(center, start);
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse();
crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse(); crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse();
@ -50,11 +50,11 @@ export const generateRFArc = (
while (!crsOrthogonalOnOrigin.equal(crsOrthogonalOnEndpoint)) { while (!crsOrthogonalOnOrigin.equal(crsOrthogonalOnEndpoint)) {
if (turnDir === 'R') { if (turnDir === 'R') {
const delta = (crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin).normaliseDegrees(); const delta = (crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin).normaliseDegrees();
crsOrthogonalOnOrigin += delta < 0.1 ? delta : 0.1; crsOrthogonalOnOrigin += delta < 1 ? delta : 1;
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees();
} else { } else {
const delta = (crsOrthogonalOnOrigin - crsOrthogonalOnEndpoint).normaliseDegrees(); const delta = (crsOrthogonalOnOrigin - crsOrthogonalOnEndpoint).normaliseDegrees();
crsOrthogonalOnOrigin -= delta < 0.1 ? delta : 0.1; crsOrthogonalOnOrigin -= delta < 1 ? delta : 1;
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees();
} }

View File

@ -1,5 +1,5 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getPreciseDistance from 'geolib/es/getPreciseDistance'; import getDistance from 'geolib/es/getDistance';
import { computeIntersection } from '../utils/computeIntersection'; import { computeIntersection } from '../utils/computeIntersection';
/** /**
@ -56,19 +56,14 @@ export const generateTangentArc = (
} }
// Generate arc // Generate arc
let arcRad = 0; const arcCenter = computeIntersection(
let arcCenter = computeIntersection(
start, start,
crsOrthogonalOnOrigin, crsOrthogonalOnOrigin,
intcArcOnCrsIntoEndpoint, intcArcOnCrsIntoEndpoint,
crsOrthogonalOnEndpoint crsOrthogonalOnEndpoint
); );
if (Math.abs(crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin) <= 0.1 && arcCenter) if (!arcCenter) return null;
arcRad = getPreciseDistance(arcCenter, start); const arcRad = getDistance(arcCenter, start);
else {
arcRad = getPreciseDistance(start, end) / 2;
arcCenter = computeDestinationPoint(start, arcRad, crsOrthogonalOnOrigin);
}
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.reciprocalCourse();
crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse(); crsOrthogonalOnEndpoint = crsOrthogonalOnEndpoint.reciprocalCourse();
@ -79,24 +74,19 @@ export const generateTangentArc = (
crsOrthogonalOnOrigin -= crsOrthogonalOnOrigin < 1 ? crsOrthogonalOnOrigin : 1; crsOrthogonalOnOrigin -= crsOrthogonalOnOrigin < 1 ? crsOrthogonalOnOrigin : 1;
} }
let lastDistance = getPreciseDistance(start, end);
while (!crsOrthogonalOnOrigin.equal(crsOrthogonalOnEndpoint)) { while (!crsOrthogonalOnOrigin.equal(crsOrthogonalOnEndpoint)) {
if (turnDir === 'R') { if (turnDir === 'R') {
const delta = (crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin).normaliseDegrees(); const delta = (crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin).normaliseDegrees();
crsOrthogonalOnOrigin += delta < 0.1 ? delta : 0.1; crsOrthogonalOnOrigin += delta < 1 ? delta : 1;
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees();
} else { } else {
const delta = (crsOrthogonalOnOrigin - crsOrthogonalOnEndpoint).normaliseDegrees(); const delta = (crsOrthogonalOnOrigin - crsOrthogonalOnEndpoint).normaliseDegrees();
crsOrthogonalOnOrigin -= delta < 0.1 ? delta : 0.1; crsOrthogonalOnOrigin -= delta < 1 ? delta : 1;
crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees(); crsOrthogonalOnOrigin = crsOrthogonalOnOrigin.normaliseDegrees();
} }
const arcFix = computeDestinationPoint(arcCenter, arcRad, crsOrthogonalOnOrigin); const arcFix = computeDestinationPoint(arcCenter, arcRad, crsOrthogonalOnOrigin);
const newDistance = getPreciseDistance(arcFix, end);
if (lastDistance <= newDistance && lastDistance < 25) break;
lastDistance = newDistance;
line.push([arcFix.longitude, arcFix.latitude]); line.push([arcFix.longitude, arcFix.latitude]);
} }
} }

View File

@ -7,11 +7,6 @@ export const TerminatorsAF = (
previousFix: NavFix, previousFix: NavFix,
waypoint?: Waypoint waypoint?: Waypoint
): [NavFix?, LineSegment[]?] => { ): [NavFix?, LineSegment[]?] => {
const navaid = {
latitude: leg.NavLat,
longitude: leg.NavLon,
};
const targetFix: NavFix = { const targetFix: NavFix = {
latitude: leg.WptLat, latitude: leg.WptLat,
longitude: leg.WptLon, longitude: leg.WptLon,
@ -21,13 +16,27 @@ export const TerminatorsAF = (
speed: computeSpeed(leg, previousFix), speed: computeSpeed(leg, previousFix),
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
const arcEndCrs = getGreatCircleBearing(navaid, targetFix); const arcEndCrs = getGreatCircleBearing(
{
latitude: leg.NavLat,
longitude: leg.NavLon,
},
{
latitude: leg.WptLat,
longitude: leg.WptLon,
}
);
const line = generateAFArc(arcEndCrs, leg.Course.toTrue(navaid), previousFix, navaid, leg.NavDist, leg.TurnDir); const line = generateAFArc(
arcEndCrs,
leg.Course.toTrue({ latitude: leg.NavLat, longitude: leg.NavLon }),
previousFix,
{ latitude: leg.NavLat, longitude: leg.NavLon },
leg.NavDist,
leg.TurnDir
);
return [targetFix, line]; return [targetFix, line];
}; };

View File

@ -31,8 +31,6 @@ export const TerminatorsCA = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -1,6 +1,6 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getDistance from 'geolib/es/getDistance';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import getPreciseDistance from 'geolib/es/getPreciseDistance';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -23,7 +23,7 @@ export const TerminatorsCD = (
// Compute distance to fly from arc end // Compute distance to fly from arc end
const crsToNavaid = getGreatCircleBearing(arcEnd, navaid); const crsToNavaid = getGreatCircleBearing(arcEnd, navaid);
const distToNavaid = getPreciseDistance(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) {
@ -44,8 +44,6 @@ export const TerminatorsCD = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -1,8 +1,7 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import getPreciseDistance from 'geolib/es/getPreciseDistance';
import Parser from '../parser'; import Parser from '../parser';
import { generateTangentArc } from '../pathGenerators/generateTangentArc'; import { computeIntersection } from '../utils/computeIntersection';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
import { computeTurnRate } from '../utils/computeTurnRate'; import { computeTurnRate } from '../utils/computeTurnRate';
@ -14,7 +13,7 @@ export const TerminatorsCF = (
): [NavFix?, LineSegment[]?] => { ): [NavFix?, LineSegment[]?] => {
const speed = computeSpeed(leg, previousFix); const speed = computeSpeed(leg, previousFix);
const crsIntoEndpoint = leg.Course.toTrue(previousFix); const crsIntoEndpoint = leg.Course.toTrue(previousFix);
const line: LineSegment[] = []; const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]];
const targetFix: NavFix = { const targetFix: NavFix = {
latitude: leg.WptLat, latitude: leg.WptLat,
@ -25,41 +24,11 @@ export const TerminatorsCF = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
const crsToIntercept = leg.Course.toTrue(targetFix); const crsToIntercept = leg.Course.toTrue(targetFix);
// Compute overfly arc // Compute overfly arc
let arc1: LineSegment[] | null = null; if (previousFix.isFlyOver && !lastCourse.equal(crsIntoEndpoint)) {
let arc2: LineSegment[] = [[previousFix.longitude, previousFix.latitude]];
if (previousFix.isFlyOver) {
arc1 = generateTangentArc(crsIntoEndpoint, lastCourse, previousFix, targetFix, leg.TurnDir);
} else {
arc1 = [[previousFix.longitude, previousFix.latitude]];
}
let arc;
if (arc1 && arc1.length > 1) {
const endCrs = getGreatCircleBearing(
{
latitude: arc1.at(-1)![1],
longitude: arc1.at(-1)![0],
},
targetFix
);
const endDist = getPreciseDistance(
{
latitude: arc1.at(-1)![1],
longitude: arc1.at(-1)![0],
},
targetFix
);
if (endDist <= 25 || (endCrs <= crsIntoEndpoint + 1 && endCrs >= crsIntoEndpoint - 1)) arc = arc1;
}
if (previousFix.isFlyOver && (!lastCourse.equal(crsIntoEndpoint) || !lastCourse.equal(crsToIntercept))) {
const turnRate = computeTurnRate(speed, Parser.AC_BANK); const turnRate = computeTurnRate(speed, Parser.AC_BANK);
let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix); let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
@ -71,48 +40,53 @@ export const TerminatorsCF = (
} }
// Generate arc // Generate arc
let lastDistance = getPreciseDistance(previousFix, targetFix);
while (!updatedCrsToIntercept.equal(crsToIntercept)) { while (!updatedCrsToIntercept.equal(crsToIntercept)) {
let interceptAngle = 0;
if (leg.TurnDir === 'R') interceptAngle = Math.abs(lastCourse - crsToIntercept);
else interceptAngle = Math.abs(crsToIntercept - lastCourse);
let time = 0; let time = 0;
const increment = 0.1;
if (interceptAngle < 44.9 || interceptAngle >= 45.1) {
if (leg.TurnDir === 'R') { if (leg.TurnDir === 'R') {
//const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
const increment = 1; //delta < 1 ? delta : 1;
lastCourse = (lastCourse + increment).normaliseDegrees(); lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} else { } else {
//const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
const increment = 1; //delta < 1 ? delta : 1;
lastCourse = (lastCourse - increment).normaliseDegrees(); lastCourse = (lastCourse - increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} }
} else time = increment / turnRate;
const arcFix = computeDestinationPoint( const arcFix = computeDestinationPoint(
{ {
latitude: arc2.at(-1)![1], latitude: line.at(-1)![1],
longitude: arc2.at(-1)![0], longitude: line.at(-1)![0],
}, },
((speed / 3600) * time).toMetre(), ((speed / 3600) * time).toMetre(),
lastCourse lastCourse
); );
arc2.push([arcFix.longitude, arcFix.latitude]); line.push([arcFix.longitude, arcFix.latitude]);
// Update previousFix // Update previousFix
previousFix.latitude = arcFix.latitude; previousFix.latitude = arcFix.latitude;
previousFix.longitude = arcFix.longitude; previousFix.longitude = arcFix.longitude;
updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix); updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
const newDistance = getPreciseDistance(previousFix, targetFix); let interceptAngle = 0;
if (lastDistance <= newDistance && lastDistance < 25) break; if (leg.TurnDir === 'R') Math.abs((interceptAngle = lastCourse - crsToIntercept));
lastDistance = newDistance; else interceptAngle = Math.abs(crsToIntercept - lastCourse);
if (interceptAngle >= 45) break;
} }
} }
if (!arc) arc = arc2; const interceptFix: NavFix = {
line.push(...arc); ...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]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -34,12 +34,12 @@ export const TerminatorsCI = (
let time = 0; let time = 0;
if (leg.TurnDir === 'R') { if (leg.TurnDir === 'R') {
const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
lastCourse = (lastCourse + increment).normaliseDegrees(); lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} else { } else {
const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
lastCourse = (lastCourse - increment).normaliseDegrees(); lastCourse = (lastCourse - increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} }
@ -68,8 +68,6 @@ export const TerminatorsCI = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([interceptFix.longitude, interceptFix.latitude]); line.push([interceptFix.longitude, interceptFix.latitude]);

View File

@ -27,8 +27,6 @@ export const TerminatorsCR = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([interceptFix.longitude, interceptFix.latitude]); line.push([interceptFix.longitude, interceptFix.latitude]);

View File

@ -1,9 +1,6 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import getPreciseDistance from 'geolib/es/getPreciseDistance'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import Parser from '../parser';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
import { computeTurnRate } from '../utils/computeTurnRate';
export const TerminatorsDF = ( export const TerminatorsDF = (
leg: DFTerminalEntry, leg: DFTerminalEntry,
@ -12,9 +9,6 @@ export const TerminatorsDF = (
waypoint?: Waypoint waypoint?: Waypoint
): [NavFix?, LineSegment[]?] => { ): [NavFix?, LineSegment[]?] => {
const speed = computeSpeed(leg, previousFix); const speed = computeSpeed(leg, previousFix);
const turnRate = computeTurnRate(speed, Parser.AC_BANK);
const originalCrsFromOrigin = lastCourse;
const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]];
const targetFix: NavFix = { const targetFix: NavFix = {
latitude: leg.WptLat, latitude: leg.WptLat,
@ -25,95 +19,20 @@ export const TerminatorsDF = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
let crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix); const crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix);
let force360 = getPreciseDistance(previousFix, targetFix) < 25;
// Check if there even is an arc // Compute overfly
if (force360 || !lastCourse.equal(crsIntoEndpoint)) { const [line, _, _lastCourse] = generateOverflyArc(
// Turn Dir crsIntoEndpoint,
if (!leg.TurnDir || leg.TurnDir === 'E') { lastCourse,
let prov = lastCourse - crsIntoEndpoint; previousFix,
prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov; speed,
leg.TurnDir = prov > 0 ? 'L' : 'R'; leg.TurnDir,
} previousFix.latitude.equal(targetFix.latitude) && previousFix.longitude.equal(targetFix.longitude)
// Generate arc
while (!lastCourse.equal(crsIntoEndpoint)) {
let time = 0;
if (leg.TurnDir === 'R') {
const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1;
lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate;
} else {
const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.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
); );
lastCourse = _lastCourse;
crsIntoEndpoint = getGreatCircleBearing(arcFix, targetFix);
line.push([arcFix.longitude, arcFix.latitude]);
// made a loop
if (line.length >= 3600) {
if (!force360) {
line.splice(1);
}
force360 = false;
break;
}
}
// Second half
if (force360) {
const temp = crsIntoEndpoint;
crsIntoEndpoint = originalCrsFromOrigin;
lastCourse = temp;
while (!lastCourse.equal(crsIntoEndpoint)) {
let time = 0;
if (leg.TurnDir === 'R') {
const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1;
lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate;
} else {
const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.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
);
crsIntoEndpoint = getGreatCircleBearing(arcFix, targetFix);
line.push([arcFix.longitude, arcFix.latitude]);
}
}
}
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -35,8 +35,6 @@ export const TerminatorsFA = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -1,8 +1,8 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
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 { 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 = (
@ -38,12 +38,12 @@ export const TerminatorsFC = (
let time = 0; let time = 0;
if (leg.TurnDir === 'R') { if (leg.TurnDir === 'R') {
const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
lastCourse = (lastCourse + increment).normaliseDegrees(); lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} else { } else {
const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
lastCourse = (lastCourse - increment).normaliseDegrees(); lastCourse = (lastCourse - increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} }
@ -87,8 +87,6 @@ export const TerminatorsFC = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -1,6 +1,6 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getDistance from 'geolib/es/getDistance';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import getPreciseDistance from 'geolib/es/getPreciseDistance';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -27,7 +27,7 @@ export const TerminatorsFD = (
// Compute distance to fly from arc end // Compute distance to fly from arc end
const crsToNavaid = getGreatCircleBearing(arcEnd, navaid); const crsToNavaid = getGreatCircleBearing(arcEnd, navaid);
const distToNavaid = getPreciseDistance(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) {
@ -48,8 +48,6 @@ export const TerminatorsFD = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -10,8 +10,6 @@ export const TerminatorsIF = (leg: IFTerminalEntry, waypoint?: Waypoint): NavFix
speed: leg.SpeedLimit ? leg.SpeedLimit : Parser.AC_SPEED, speed: leg.SpeedLimit ? leg.SpeedLimit : Parser.AC_SPEED,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
return targetFix; return targetFix;

View File

@ -1,7 +1,6 @@
import { generateRFArc } from '../pathGenerators/generateRFArc'; import { generateRFArc } from '../pathGenerators/generateRFArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
// NOTE: Direct entry into an RF does not calculate a usable line, given inbound course is unknown.
export const TerminatorsRF = ( export const TerminatorsRF = (
leg: RFTerminalEntry, leg: RFTerminalEntry,
previousFix: NavFix, previousFix: NavFix,
@ -17,8 +16,6 @@ export const TerminatorsRF = (
speed: computeSpeed(leg, previousFix), speed: computeSpeed(leg, previousFix),
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
const line = generateRFArc( const line = generateRFArc(

View File

@ -1,10 +1,7 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import getPreciseDistance from 'geolib/es/getPreciseDistance';
import Parser from '../parser'; import Parser from '../parser';
import { generateTangentArc } from '../pathGenerators/generateTangentArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
import { computeTurnRate } from '../utils/computeTurnRate'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
export const TerminatorsTF = ( export const TerminatorsTF = (
leg: TFTerminalEntry, leg: TFTerminalEntry,
@ -12,56 +9,26 @@ export const TerminatorsTF = (
lastCourse: number, lastCourse: number,
waypoint?: Waypoint waypoint?: Waypoint
): [NavFix?, LineSegment[]?] => { ): [NavFix?, LineSegment[]?] => {
const speed = computeSpeed(leg, previousFix);
const line: LineSegment[] = [];
const targetFix: NavFix = { const targetFix: NavFix = {
latitude: leg.WptLat, latitude: leg.WptLat,
longitude: leg.WptLon, longitude: leg.WptLon,
name: waypoint?.Ident ?? undefined, name: waypoint?.Ident ?? undefined,
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: computeSpeed(leg, previousFix),
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
const crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix);
// Compute overfly arc const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]];
let arc1: LineSegment[] | null = null;
let arc2: LineSegment[] = [[previousFix.longitude, previousFix.latitude]]; const trackIntoEndpoint = getGreatCircleBearing(previousFix, targetFix);
if (previousFix.isFlyOver) { if (previousFix.isFlyOver) {
arc1 = generateTangentArc(crsIntoEndpoint, lastCourse, previousFix, targetFix, leg.TurnDir); let crsIntoEndpoint = trackIntoEndpoint;
} else {
arc1 = [[previousFix.longitude, previousFix.latitude]];
}
let arc;
if (arc1 && arc1.length > 1) {
const endCrs = getGreatCircleBearing(
{
latitude: arc1.at(-1)![1],
longitude: arc1.at(-1)![0],
},
targetFix
);
const endDist = getPreciseDistance(
{
latitude: arc1.at(-1)![1],
longitude: arc1.at(-1)![0],
},
targetFix
);
if (endDist <= 25 || (endCrs <= crsIntoEndpoint + 1 && endCrs >= crsIntoEndpoint - 1)) arc = arc1;
}
if (previousFix.isFlyOver && (!lastCourse.equal(crsIntoEndpoint) || !lastCourse.equal(crsIntoEndpoint))) {
const turnRate = computeTurnRate(speed, Parser.AC_BANK);
let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
// Check if there even is an arc
if (crsIntoEndpoint !== lastCourse) {
// Turn Dir // Turn Dir
if (!leg.TurnDir || leg.TurnDir === 'E') { if (!leg.TurnDir || leg.TurnDir === 'E') {
let prov = lastCourse - crsIntoEndpoint; let prov = lastCourse - crsIntoEndpoint;
@ -70,49 +37,40 @@ export const TerminatorsTF = (
} }
// Generate arc // Generate arc
let lastDistance = getPreciseDistance(previousFix, targetFix); let condition = false;
while (!updatedCrsToIntercept.equal(crsIntoEndpoint)) { do {
let interceptAngle = 0;
if (leg.TurnDir === 'R') interceptAngle = Math.abs(lastCourse - crsIntoEndpoint);
else interceptAngle = Math.abs(crsIntoEndpoint - lastCourse);
let time = 0;
const increment = 0.1;
if (interceptAngle < 44.9 || interceptAngle >= 45.1) {
if (leg.TurnDir === 'R') { if (leg.TurnDir === 'R') {
lastCourse = (lastCourse + increment).normaliseDegrees(); const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
time = increment / turnRate; lastCourse += delta < 1 ? delta : 1;
lastCourse = lastCourse.normaliseDegrees();
} else { } else {
lastCourse = (lastCourse - increment).normaliseDegrees(); const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
time = increment / turnRate; lastCourse -= delta < 1 ? delta : 1;
lastCourse = lastCourse.normaliseDegrees();
} }
} else time = increment / turnRate;
const arcFix = computeDestinationPoint( const arcFix = computeDestinationPoint(
{ {
latitude: arc2.at(-1)![1], latitude: line.at(-1)![1],
longitude: arc2.at(-1)![0], longitude: line.at(-1)![0],
}, },
((speed / 3600) * time).toMetre(), ((previousFix.speed ? previousFix.speed : Parser.AC_SPEED) / 3600).toMetre(),
lastCourse lastCourse
); );
arc2.push([arcFix.longitude, arcFix.latitude]); line.push([arcFix.longitude, arcFix.latitude]);
// Update previousFix crsIntoEndpoint = getGreatCircleBearing(arcFix, targetFix);
previousFix.latitude = arcFix.latitude;
previousFix.longitude = arcFix.longitude;
updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
const newDistance = getPreciseDistance(previousFix, targetFix); if (leg.TurnDir === 'R') {
if (lastDistance <= newDistance && lastDistance < 25) break; condition = crsIntoEndpoint > trackIntoEndpoint;
lastDistance = newDistance; } else {
condition = crsIntoEndpoint < trackIntoEndpoint;
}
} while (condition);
} }
} }
if (!arc) arc = arc2;
line.push(...arc);
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);
return [targetFix, line]; return [targetFix, line];

View File

@ -32,8 +32,6 @@ export const TerminatorsVA = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -1,6 +1,6 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getDistance from 'geolib/es/getDistance';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import getPreciseDistance from 'geolib/es/getPreciseDistance';
import { generateOverflyArc } from '../pathGenerators/generateOverflyArc'; import { generateOverflyArc } from '../pathGenerators/generateOverflyArc';
import { computeSpeed } from '../utils/computeSpeed'; import { computeSpeed } from '../utils/computeSpeed';
@ -24,7 +24,7 @@ export const TerminatorsVD = (
// Compute distance to fly from arc end // Compute distance to fly from arc end
const crsToNavaid = getGreatCircleBearing(arcEnd, navaid); const crsToNavaid = getGreatCircleBearing(arcEnd, navaid);
const distToNavaid = getPreciseDistance(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) {
@ -45,8 +45,6 @@ export const TerminatorsVD = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -1,10 +1,10 @@
import computeDestinationPoint from 'geolib/es/computeDestinationPoint';
import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import Parser from '../parser'; 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 { 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 = (
@ -35,12 +35,12 @@ export const TerminatorsVI = (
let time = 0; let time = 0;
if (leg.TurnDir === 'R') { if (leg.TurnDir === 'R') {
const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees(); const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
lastCourse = (lastCourse + increment).normaliseDegrees(); lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} else { } else {
const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees(); const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
const increment = delta < 0.1 ? delta : 0.1; const increment = delta < 1 ? delta : 1;
lastCourse = (lastCourse - increment).normaliseDegrees(); lastCourse = (lastCourse - increment).normaliseDegrees();
time = increment / turnRate; time = increment / turnRate;
} }
@ -69,8 +69,6 @@ export const TerminatorsVI = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([interceptFix.longitude, interceptFix.latitude]); line.push([interceptFix.longitude, interceptFix.latitude]);

View File

@ -28,8 +28,6 @@ export const TerminatorsVR = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
line.push([interceptFix.longitude, interceptFix.latitude]); line.push([interceptFix.longitude, interceptFix.latitude]);

View File

@ -5,12 +5,7 @@
* @param brng2 bearing from Point 2 * @param brng2 bearing from Point 2
* @returns Intersection point * @returns Intersection point
*/ */
export const computeIntersection = ( export const computeIntersection = (p1: NavFix, brng1: number, p2: NavFix, brng2: number): NavFix | undefined => {
p1: NavFix,
brng1: number,
p2: NavFix,
brng2: number
): NavFix | undefined | null => {
if (isNaN(brng1)) throw new TypeError(`invalid brng1 ${brng1}`); if (isNaN(brng1)) throw new TypeError(`invalid brng1 ${brng1}`);
if (isNaN(brng2)) throw new TypeError(`invalid brng2 ${brng2}`); if (isNaN(brng2)) throw new TypeError(`invalid brng2 ${brng2}`);
@ -48,7 +43,7 @@ export const computeIntersection = (
const α2 = θ21 - θ23; // angle 1-2-3 const α2 = θ21 - θ23; // angle 1-2-3
if (Math.sin(α1) == 0 && Math.sin(α2) == 0) return undefined; // infinite intersections if (Math.sin(α1) == 0 && Math.sin(α2) == 0) return undefined; // infinite intersections
if (Math.sin(α1) * Math.sin(α2) < 0) return p2; // ambiguous intersection (antipodal/360°) if (Math.sin(α1) * Math.sin(α2) < 0) return undefined; // ambiguous intersection (antipodal/360°)
const cosα3 = -Math.cos(α1) * Math.cos(α2) + Math.sin(α1) * Math.sin(α2) * Math.cos(δ12); const cosα3 = -Math.cos(α1) * Math.cos(α2) + Math.sin(α1) * Math.sin(α2) * Math.cos(δ12);

View File

@ -1,15 +1 @@
@import 'tailwindcss'; @import 'tailwindcss';
@theme {
--animate-fade-in-scale: fade-in-scale 0.3s ease-out;
@keyframes fade-in-scale {
0% {
opacity: 0;
transform: scale(0.95);
}
100% {
opacity: 1;
transform: scale(1);
}
}
}

View File

@ -75,7 +75,6 @@ export declare global {
ICAO: string; ICAO: string;
FullName: string; FullName: string;
RwyID?: number; RwyID?: number;
Proc: 1 | 2 | 3;
}; };
type NavFix = { type NavFix = {
@ -85,10 +84,9 @@ export declare global {
speed?: number; speed?: number;
name?: string; name?: string;
isFlyOver?: boolean; isFlyOver?: boolean;
'marker-color'?: string;
altitudeConstraint?: string; altitudeConstraint?: string;
speedConstraint?: number; speedConstraint?: number;
IsFAF?: boolean;
IsMAP?: boolean;
// For map // For map
isIntersection?: boolean; isIntersection?: boolean;
}; };