MAP rendering

More CF/TF fixes
This commit is contained in:
Kilian Hofmann 2025-07-17 15:33:02 +02:00
parent 779a78649e
commit c5cd3c7a0e
28 changed files with 540 additions and 332 deletions

View File

@ -100,7 +100,6 @@ 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)
@ -320,7 +319,6 @@ 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)
@ -626,7 +624,6 @@ 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)
@ -700,4 +697,9 @@ LIMC 35R MMP8G SID (Cycle 2507, ID 11909)
### Notes ### Notes
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 (
<> <>
{transitions.length === 0 ? ( {!user ? (
<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,7 +38,27 @@ function App() {
</a> </a>
</> </>
)} )}
{user && ( </div>
) : (
<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}
@ -47,39 +67,29 @@ function App() {
setSelectedRunway={setSelectedRunway} setSelectedRunway={setSelectedRunway}
setSelectedTerminal={setSelectedTerminal} setSelectedTerminal={setSelectedTerminal}
handleSelection={(selectedTransitions) => { handleSelection={(selectedTransitions) => {
const _transitions = selectedTransitions.map((transition) => ({ let _transitions = selectedTransitions
name: transition, .map((transition) => ({
data: parser.parse(selectedRunway!, transition), name: 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);
}} }}
/> />
)} )}
</div> <Map
) : ( airport={selectedAirport}
<div className="flex h-dvh w-dvw"> chart={selectedChart}
{transitions.length > 0 && selectedAirport && selectedTerminal ? ( procedures={selectedTransition ? [selectedTransition] : []}
<> />
<Sidebar
airport={selectedAirport}
terminal={selectedTerminal}
transitions={transitions}
transition={selectedTransition}
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>
)}
</div> </div>
)} )}
</> </>

View File

@ -0,0 +1,22 @@
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,22 +1,26 @@
import { default as L } from 'leaflet'; import { default as L } from 'leaflet';
import 'leaflet-svg-shape-markers'; import 'leaflet-svg-shape-markers';
import { createRef, type FC } from 'react'; import { createRef, Fragment, useEffect, 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; airport: Airport | undefined;
chart: Chart | undefined; chart: Chart | undefined;
procedure: Procedure | undefined; procedures: Procedure[];
} }
export const Map: FC<MapProps> = ({ airport, chart, procedure }) => { export const Map: FC<MapProps> = ({ airport, chart, procedures }) => {
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={[airport.Latitude, airport.Longitude]} center={[0, 0]}
zoom={13} zoom={5}
zoomSnap={0} zoomSnap={0}
className="h-full w-full" className="h-full w-full"
ref={(_mapRef) => { ref={(_mapRef) => {
@ -46,15 +50,15 @@ export const Map: FC<MapProps> = ({ airport, chart, procedure }) => {
/> />
)} )}
{procedure && ( {procedures.map((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: '#ff00ff', color: properties.isMAP ? '#00ffff' : '#ff00ff',
stroke: true, stroke: true,
weight: 5, weight: properties.isMAP ? 2.5 : 5,
opacity: 1, opacity: 1,
dashArray: properties.isManual ? '20, 20' : undefined, dashArray: properties.isManual ? '20, 20' : undefined,
})} })}
@ -71,12 +75,22 @@ export const Map: FC<MapProps> = ({ airport, chart, procedure }) => {
weight: 3, weight: 3,
}} }}
pointToLayer={({ properties }, latlng) => { pointToLayer={({ properties }, latlng) => {
if (properties.isFlyOver) if (properties.isIntersection) return L.circleMarker(latlng, { radius: 6 });
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',
@ -98,8 +112,8 @@ export const Map: FC<MapProps> = ({ airport, chart, procedure }) => {
}} }}
filter={(feature) => feature.geometry.type === 'Point'} filter={(feature) => feature.geometry.type === 'Point'}
/> />
</> </Fragment>
)} ))}
</MapContainer> </MapContainer>
); );
}; };

View File

@ -1,5 +1,6 @@
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();
@ -24,26 +25,32 @@ 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 w-full flex-col items-center justify-center gap-2 p-2"> <div className="flex h-full w-[300px] shrink-0 flex-col overflow-hidden">
{selectedAirport && ( {selectedAirport && (
<button <button
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" 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={() => { onClick={() => {
setError(undefined); setError(undefined);
setInFlight(undefined);
if (selectedTerminal) { if (selectedTerminal) {
setSelectedTerminal(undefined); setSelectedTerminal(undefined);
@ -58,68 +65,126 @@ export const ProcedureSelect: FC<ProcedureSelectProps> = ({
</button> </button>
)} )}
{!selectedAirport && ( <div className="flex h-[calc(100%-50px)] flex-col gap-2 overflow-y-auto px-2 pb-2">
<div className="flex w-full flex-col gap-2"> {!selectedAirport && (
<h1 className="text-center text-3xl">Enter Airport ICAO</h1> <div className="flex w-full flex-col gap-2">
<input <h1 className="text-center text-3xl">Enter Airport ICAO</h1>
ref={inputRef} <input
className="rounded border border-black px-2 py-1 focus:outline-2 focus-visible:outline-2" ref={inputRef}
onChange={(e) => { className="rounded border border-black px-2 py-1 focus:outline-2 focus-visible:outline-2"
if (e.target.value.length <= 4) e.target.value = e.target.value.toUpperCase(); onChange={(e) => {
else e.target.value = e.target.value.slice(0, 4); if (e.target.value.length <= 4) e.target.value = e.target.value.toUpperCase();
}} else e.target.value = e.target.value.slice(0, 4);
></input> }}
<button ></input>
className="w-full cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus-visible:outline-2"
onClick={() => {
const airport = parser.airports.find(({ ICAO }) => ICAO === inputRef.current?.value.toUpperCase());
if (!airport) {
setError('Airport not found');
return;
}
setSelectedAirport(airport);
setError(undefined);
}}
>
Select Airport
</button>
{error && <span className="text-center text-red-700">{error}</span>}
</div>
)}
{selectedAirport && !selectedRunway && (
<div className="flex w-full flex-col gap-2">
<h1 className="text-center text-3xl">Select Runway</h1>
{runways.map((runway) => (
<button <button
key={runway.ID}
className="w-full cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus-visible:outline-2" className="w-full cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus-visible:outline-2"
onClick={() => setSelectedRunway(runway)}
>
Runway {runway.Ident}
</button>
))}
</div>
)}
{selectedAirport && selectedRunway && !selectedTerminal && (
<div className="flex w-full flex-col gap-2">
<h1 className="text-center text-3xl">Select Procedure</h1>
{terminals.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"
onClick={() => { onClick={() => {
parser.loadTerminal(terminal.ID).then(() => { const airport = parser.airports.find(({ ICAO }) => ICAO === inputRef.current?.value.toUpperCase());
setSelectedTerminal(terminal); if (!airport) {
const transitions = new Set(parser.procedures.map((proc) => proc.Transition)); setError('Airport not found');
handleSelection(Array.from(transitions)); return;
}); }
setSelectedAirport(airport);
setError(undefined);
}} }}
> >
{terminal.FullName} Select Airport
</button> </button>
))} {error && <span className="text-center text-red-700">{error}</span>}
</div> </div>
)} )}
{selectedAirport && !selectedRunway && (
<div className="flex w-full flex-col gap-2">
<h1 className="text-center text-3xl">Select Runway</h1>
{runways.map((runway) => (
<button
key={runway.ID}
className="w-full cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus-visible:outline-2"
onClick={() => setSelectedRunway(runway)}
>
Runway {runway.Ident}
</button>
))}
</div>
)}
{selectedAirport && selectedRunway && !selectedTerminal && (
<div className="flex w-full flex-col gap-2">
<h1 className="text-center text-3xl">Select Procedure</h1>
<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
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={() => {
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={() => {
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 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,11 +1,13 @@
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 { useEffect, useState, type Dispatch, type FC, type SetStateAction } from 'react'; import { useCallback, 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;
@ -17,6 +19,7 @@ interface SidebarProps {
export const Sidebar: FC<SidebarProps> = ({ export const Sidebar: FC<SidebarProps> = ({
airport, airport,
runway,
terminal, terminal,
transitions, transitions,
transition, transition,
@ -25,14 +28,83 @@ export const Sidebar: FC<SidebarProps> = ({
setChart, setChart,
backAction, backAction,
}) => { }) => {
const [chartIndex, setChartIndex] = useState<NGChart[]>([]); const [inFlight, setInFlight] = useState(false);
const [chartIndex, setChartIndex] = useState<{
sid: NGChart[];
star: NGChart[];
iap: NGChart[];
}>({ star: [], sid: [], iap: [] });
useEffect(() => { useEffect(() => {
setInFlight(true);
(async () => { (async () => {
setChartIndex((await charts.getChartsIndex({ icao: airport.ICAO, version: 'STD' })) ?? []); const _chartIndex = 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
@ -57,51 +129,65 @@ export const Sidebar: FC<SidebarProps> = ({
setTransition(_procedure); setTransition(_procedure);
}} }}
> >
{_procedure.name ? _procedure.name : 'ZZZZ'} {_procedure.name ? _procedure.name : 'NONE'}
</button> </button>
))} ))}
</div> </div>
<div className="flex flex-col gap-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">Charts</div> <div className="sticky top-0 z-10 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">Charts</div>
{chartIndex {inFlight && <Loader size={3} />}
.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; <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)
.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>
const chartImage = await charts.getChartImage({ chart: _chart, theme: 'light' }); <div className="flex flex-col gap-2">
if (!chartImage) return; <div className="sticky top-7 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">Departures</div>
// Crop {chartIndex.sid
const dataURL = await new BrowserImageManipulation() .filter((_chart) => _chart.is_georeferenced)
.loadBlob(chartImage) .map((_chart) => (
.crop( <button
planView.pixels.x2 - planView.pixels.x1, key={_chart.index_number}
planView.pixels.y1 - planView.pixels.y2, 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' : ''}`}
planView.pixels.x1, onClick={() => findChart(_chart)}
planView.pixels.y2 >
) <span className="font-bold">{_chart.index_number}</span>
.saveAsImage(); <br />
{_chart.name}
</button>
))}
</div>
const bounds = new L.LatLngBounds( <div className="flex flex-col gap-2">
[planView.latlng.lat1, planView.latlng.lng1], <div className="sticky top-7 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">Approaches</div>
[planView.latlng.lat2, planView.latlng.lng2] {chartIndex.iap
); .filter((_chart) => _chart.is_georeferenced)
.map((_chart) => (
setChart({ data: dataURL, index_number: _chart.index_number, bounds }); <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' : ''}`}
<span className="font-bold">{_chart.index_number}</span> onClick={() => findChart(_chart)}
<br /> >
{_chart.name} <span className="font-bold">{_chart.index_number}</span>
</button> <br />
))} {_chart.name}
</button>
))}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -128,31 +128,32 @@ 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( const procedure = this._procedures.filter(
({ Transition }) => !Transition || Transition === transition || Transition === 'ALL' ({ Transition }) => !Transition || Transition === transition || Transition === 'ALL'
); );
// Main // Main
for (let index = 0; index < procedure.length; index++) { for (let index = 0; index < procedure.length; index++) {
//lastCourse = runway.TrueHeading;
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.find(({ ID }) => ID === leg.WptID); 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); update(fixToAdd, lineToAdd, legOptions);
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); update(fixToAdd, lineToAdd, legOptions);
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); update(fixToAdd, lineToAdd, legOptions);
break; break;
} }
case 'CF': { case 'CF': {
@ -162,7 +163,7 @@ class Parser {
lastCourse, lastCourse,
waypoint waypoint
); );
update(fixToAdd, lineToAdd); update(fixToAdd, lineToAdd, legOptions);
break; break;
} }
case 'CI': { case 'CI': {
@ -172,37 +173,37 @@ class Parser {
{ ...previousFix }, // COPY { ...previousFix }, // COPY
lastCourse lastCourse
); );
update(fixToAdd, lineToAdd); update(fixToAdd, lineToAdd, legOptions);
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); update(fixToAdd, lineToAdd, legOptions);
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); update(fixToAdd, lineToAdd, legOptions);
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); update(fixToAdd, lineToAdd, legOptions);
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); update(fixToAdd, lineToAdd, legOptions);
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); update(fixToAdd, lineToAdd, legOptions);
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 }); update(fixToAdd, lineToAdd, { isManual: true, ...legOptions });
// Make overfly // Make overfly
navFixes.at(-1)!.isFlyOver = true; navFixes.at(-1)!.isFlyOver = true;
break; break;
@ -231,7 +232,7 @@ class Parser {
lastCourse = (leg as RFTerminalEntry).Course?.toTrue(fixToAdd); lastCourse = (leg as RFTerminalEntry).Course?.toTrue(fixToAdd);
} }
if (lineToAdd) { if (lineToAdd) {
lineSegments.push({ line: lineToAdd }); lineSegments.push({ line: lineToAdd, ...legOptions });
} }
break; break;
} }
@ -242,17 +243,17 @@ class Parser {
lastCourse, lastCourse,
waypoint waypoint
); );
update(fixToAdd, lineToAdd); 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); update(fixToAdd, lineToAdd, legOptions);
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); update(fixToAdd, lineToAdd, legOptions);
break; break;
} }
case 'VI': { case 'VI': {
@ -262,19 +263,19 @@ class Parser {
{ ...previousFix }, // COPY { ...previousFix }, // COPY
lastCourse lastCourse
); );
update(fixToAdd, lineToAdd); update(fixToAdd, lineToAdd, legOptions);
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 }); update(fixToAdd, lineToAdd, { isManual: true, ...legOptions });
// Make overfly // Make overfly
navFixes.at(-1)!.isFlyOver = true; 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); update(fixToAdd, lineToAdd, legOptions);
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 * @param turnDir Turn direction
* @returns Line segments * @returns Line segments
*/ */
export const generateAFArc = ( export const generateAFArc = (

View File

@ -56,14 +56,15 @@ export const generateTangentArc = (
} }
// Generate arc // Generate arc
let arcRad = 0;
let arcCenter = computeIntersection( let arcCenter = computeIntersection(
start, start,
crsOrthogonalOnOrigin, crsOrthogonalOnOrigin,
intcArcOnCrsIntoEndpoint, intcArcOnCrsIntoEndpoint,
crsOrthogonalOnEndpoint crsOrthogonalOnEndpoint
); );
let arcRad = 0; if (Math.abs(crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin) <= 0.1 && arcCenter)
if (arcCenter) arcRad = getPreciseDistance(arcCenter, start); arcRad = getPreciseDistance(arcCenter, start);
else { else {
arcRad = getPreciseDistance(start, end) / 2; arcRad = getPreciseDistance(start, end) / 2;
arcCenter = computeDestinationPoint(start, arcRad, crsOrthogonalOnOrigin); arcCenter = computeDestinationPoint(start, arcRad, crsOrthogonalOnOrigin);

View File

@ -7,6 +7,11 @@ 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,
@ -16,27 +21,13 @@ 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( const arcEndCrs = getGreatCircleBearing(navaid, targetFix);
{
latitude: leg.NavLat,
longitude: leg.NavLon,
},
{
latitude: leg.WptLat,
longitude: leg.WptLon,
}
);
const line = generateAFArc( const line = generateAFArc(arcEndCrs, leg.Course.toTrue(navaid), previousFix, navaid, leg.NavDist, leg.TurnDir);
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,6 +31,8 @@ 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

@ -44,6 +44,8 @@ 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

@ -3,7 +3,6 @@ import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import getPreciseDistance from 'geolib/es/getPreciseDistance'; import getPreciseDistance from 'geolib/es/getPreciseDistance';
import Parser from '../parser'; import Parser from '../parser';
import { generateTangentArc } from '../pathGenerators/generateTangentArc'; 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';
@ -26,6 +25,8 @@ 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);
@ -38,74 +39,6 @@ export const TerminatorsCF = (
arc1 = [[previousFix.longitude, previousFix.latitude]]; arc1 = [[previousFix.longitude, previousFix.latitude]];
} }
if (previousFix.isFlyOver && (!lastCourse.equal(crsIntoEndpoint) || !lastCourse.equal(crsToIntercept))) {
const turnRate = computeTurnRate(speed, Parser.AC_BANK);
let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
// 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 (!updatedCrsToIntercept.equal(crsToIntercept)) {
let time = 0;
if (leg.TurnDir === 'R') {
//const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
const increment = 0.1; //delta < 1 ? delta : 1;
lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate;
} else {
//const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
const increment = 0.1; //delta < 1 ? delta : 1;
lastCourse = (lastCourse - increment).normaliseDegrees();
time = increment / turnRate;
}
const arcFix = computeDestinationPoint(
{
latitude: arc2.at(-1)![1],
longitude: arc2.at(-1)![0],
},
((speed / 3600) * time).toMetre(),
lastCourse
);
arc2.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') interceptAngle = lastCourse - crsToIntercept;
else interceptAngle = crsToIntercept - lastCourse;
if (interceptAngle > 0 && interceptAngle <= 45) {
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) arc2.push([interceptFix.longitude, interceptFix.latitude]);
break;
}
}
}
// Decide on arc
let arc; let arc;
if (arc1 && arc1.length > 1) { if (arc1 && arc1.length > 1) {
const endCrs = getGreatCircleBearing( const endCrs = getGreatCircleBearing(
@ -124,10 +57,61 @@ export const TerminatorsCF = (
); );
if (endDist <= 25 || (endCrs <= crsIntoEndpoint + 1 && endCrs >= crsIntoEndpoint - 1)) arc = arc1; if (endDist <= 25 || (endCrs <= crsIntoEndpoint + 1 && endCrs >= crsIntoEndpoint - 1)) arc = arc1;
else arc = arc2;
} else {
arc = arc2;
} }
if (previousFix.isFlyOver && (!lastCourse.equal(crsIntoEndpoint) || !lastCourse.equal(crsToIntercept))) {
const turnRate = computeTurnRate(speed, Parser.AC_BANK);
let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
// Turn Dir
if (!leg.TurnDir || leg.TurnDir === 'E') {
let prov = lastCourse - crsIntoEndpoint;
prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov;
leg.TurnDir = prov > 0 ? 'L' : 'R';
}
// Generate arc
let lastDistance = getPreciseDistance(previousFix, targetFix);
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;
const increment = 0.1;
if (interceptAngle < 44.9 || interceptAngle >= 45.1) {
if (leg.TurnDir === 'R') {
lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate;
} else {
lastCourse = (lastCourse - increment).normaliseDegrees();
time = increment / turnRate;
}
} else time = increment / turnRate;
const arcFix = computeDestinationPoint(
{
latitude: arc2.at(-1)![1],
longitude: arc2.at(-1)![0],
},
((speed / 3600) * time).toMetre(),
lastCourse
);
arc2.push([arcFix.longitude, arcFix.latitude]);
// Update previousFix
previousFix.latitude = arcFix.latitude;
previousFix.longitude = arcFix.longitude;
updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
const newDistance = getPreciseDistance(previousFix, targetFix);
if (lastDistance <= newDistance && lastDistance < 25) break;
lastDistance = newDistance;
}
}
if (!arc) arc = arc2;
line.push(...arc); line.push(...arc);
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -68,6 +68,8 @@ 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,6 +27,8 @@ 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

@ -25,6 +25,8 @@ 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); let crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix);

View File

@ -35,6 +35,8 @@ 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

@ -87,6 +87,8 @@ 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

@ -48,6 +48,8 @@ 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,6 +10,8 @@ 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

@ -17,6 +17,8 @@ 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

@ -3,7 +3,6 @@ import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing';
import getPreciseDistance from 'geolib/es/getPreciseDistance'; import getPreciseDistance from 'geolib/es/getPreciseDistance';
import Parser from '../parser'; import Parser from '../parser';
import { generateTangentArc } from '../pathGenerators/generateTangentArc'; 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';
@ -25,6 +24,8 @@ export const TerminatorsTF = (
speed: speed, speed: speed,
speedConstraint: leg.SpeedLimit, speedConstraint: leg.SpeedLimit,
altitudeConstraint: leg.Alt, altitudeConstraint: leg.Alt,
IsFAF: leg.IsFAF,
IsMAP: leg.IsMAP,
}; };
const crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix); const crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix);
@ -37,69 +38,6 @@ export const TerminatorsTF = (
arc1 = [[previousFix.longitude, previousFix.latitude]]; arc1 = [[previousFix.longitude, previousFix.latitude]];
} }
if (previousFix.isFlyOver && (!lastCourse.equal(crsIntoEndpoint) || !lastCourse.equal(crsIntoEndpoint))) {
const turnRate = computeTurnRate(speed, Parser.AC_BANK);
let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
// 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 (!updatedCrsToIntercept.equal(crsIntoEndpoint)) {
let time = 0;
if (leg.TurnDir === 'R') {
//const delta = (crsIntoEndpoint - lastCourse).normaliseDegrees();
const increment = 0.1; //delta < 1 ? delta : 1;
lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate;
} else {
//const delta = (lastCourse - crsIntoEndpoint).normaliseDegrees();
const increment = 0.1; //delta < 1 ? delta : 1;
lastCourse = (lastCourse - increment).normaliseDegrees();
time = increment / turnRate;
}
const arcFix = computeDestinationPoint(
{
latitude: arc2.at(-1)![1],
longitude: arc2.at(-1)![0],
},
((speed / 3600) * time).toMetre(),
lastCourse
);
arc2.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') interceptAngle = lastCourse - crsIntoEndpoint;
else interceptAngle = crsIntoEndpoint - lastCourse;
if (interceptAngle > 0 && interceptAngle <= 45) {
const interceptFix: NavFix = {
...computeIntersection(previousFix, crsIntoEndpoint, targetFix, crsIntoEndpoint.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]);
break;
}
}
}
// Decide on arc
let arc; let arc;
if (arc1 && arc1.length > 1) { if (arc1 && arc1.length > 1) {
const endCrs = getGreatCircleBearing( const endCrs = getGreatCircleBearing(
@ -118,10 +56,61 @@ export const TerminatorsTF = (
); );
if (endDist <= 25 || (endCrs <= crsIntoEndpoint + 1 && endCrs >= crsIntoEndpoint - 1)) arc = arc1; if (endDist <= 25 || (endCrs <= crsIntoEndpoint + 1 && endCrs >= crsIntoEndpoint - 1)) arc = arc1;
else arc = arc2;
} else {
arc = arc2;
} }
if (previousFix.isFlyOver && (!lastCourse.equal(crsIntoEndpoint) || !lastCourse.equal(crsIntoEndpoint))) {
const turnRate = computeTurnRate(speed, Parser.AC_BANK);
let updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
// Turn Dir
if (!leg.TurnDir || leg.TurnDir === 'E') {
let prov = lastCourse - crsIntoEndpoint;
prov = prov > 180 ? prov - 360 : prov <= -180 ? prov + 360 : prov;
leg.TurnDir = prov > 0 ? 'L' : 'R';
}
// Generate arc
let lastDistance = getPreciseDistance(previousFix, targetFix);
while (!updatedCrsToIntercept.equal(crsIntoEndpoint)) {
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') {
lastCourse = (lastCourse + increment).normaliseDegrees();
time = increment / turnRate;
} else {
lastCourse = (lastCourse - increment).normaliseDegrees();
time = increment / turnRate;
}
} else time = increment / turnRate;
const arcFix = computeDestinationPoint(
{
latitude: arc2.at(-1)![1],
longitude: arc2.at(-1)![0],
},
((speed / 3600) * time).toMetre(),
lastCourse
);
arc2.push([arcFix.longitude, arcFix.latitude]);
// Update previousFix
previousFix.latitude = arcFix.latitude;
previousFix.longitude = arcFix.longitude;
updatedCrsToIntercept = getGreatCircleBearing(previousFix, targetFix);
const newDistance = getPreciseDistance(previousFix, targetFix);
if (lastDistance <= newDistance && lastDistance < 25) break;
lastDistance = newDistance;
}
}
if (!arc) arc = arc2;
line.push(...arc); line.push(...arc);
line.push([targetFix.longitude, targetFix.latitude]); line.push([targetFix.longitude, targetFix.latitude]);

View File

@ -32,6 +32,8 @@ 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

@ -45,6 +45,8 @@ 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

@ -69,6 +69,8 @@ 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,6 +28,8 @@ 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

@ -1 +1,15 @@
@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

@ -85,9 +85,10 @@ 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;
}; };