From c5cd3c7a0ed7527b5e1a4c0f487f4fb0c294f675 Mon Sep 17 00:00:00 2001 From: Kilian Hofmann Date: Thu, 17 Jul 2025 15:33:02 +0200 Subject: [PATCH] MAP rendering More CF/TF fixes --- README.md | 10 +- browser/src/App.tsx | 68 +++--- browser/src/components/Loader.tsx | 22 ++ browser/src/components/Map.tsx | 42 ++-- browser/src/components/ProcedureSelect.tsx | 195 ++++++++++++------ browser/src/components/Sidebar.tsx | 166 +++++++++++---- browser/src/parser/parser.ts | 39 ++-- .../parser/pathGenerators/generateAFArc.ts | 2 +- .../pathGenerators/generateTangentArc.ts | 5 +- browser/src/parser/terminators/AF.ts | 27 +-- browser/src/parser/terminators/CA.ts | 2 + browser/src/parser/terminators/CD.ts | 2 + browser/src/parser/terminators/CF.ts | 128 +++++------- browser/src/parser/terminators/CI.ts | 2 + browser/src/parser/terminators/CR.ts | 2 + browser/src/parser/terminators/DF.ts | 2 + browser/src/parser/terminators/FA.ts | 2 + browser/src/parser/terminators/FC.ts | 2 + browser/src/parser/terminators/FD.ts | 2 + browser/src/parser/terminators/IF.ts | 2 + browser/src/parser/terminators/RF.ts | 2 + browser/src/parser/terminators/TF.ts | 123 +++++------ browser/src/parser/terminators/VA.ts | 2 + browser/src/parser/terminators/VD.ts | 2 + browser/src/parser/terminators/VI.ts | 2 + browser/src/parser/terminators/VR.ts | 2 + browser/src/style.css | 14 ++ browser/src/types/navdata.d.ts | 3 +- 28 files changed, 540 insertions(+), 332 deletions(-) create mode 100644 browser/src/components/Loader.tsx diff --git a/README.md b/README.md index 10ce689..ce04131 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,6 @@ LGAV 21L BIBE2F SID (Cycle 2507, ID 10657) The leg terminates upon reaching `Distance`. This intercept point then becomes the origin fix of the succeeding leg. -This new origin is an implicit overfly. ## Course to Fix (CF) @@ -320,7 +319,6 @@ LGAV 03R BIBE2T SID (Cycle 2507, ID 10659) The leg terminates upon reaching `Distance`. 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) @@ -626,7 +624,6 @@ LFRK 31 NEVI4Y SID (Cycle 2507, ID 10482) The leg terminates upon reaching `Distance`. This intercept point then becomes the origin fix of the succeeding leg. -This new origin is an implicit overfly. ## Heading to Intercept (VI) @@ -700,4 +697,9 @@ LIMC 35R MMP8G SID (Cycle 2507, ID 11909) ### Notes The leg terminates at the intercept point. -This intercept point then becomes the origin fix of the succeeding leg. \ No newline at end of file +This intercept point then becomes the origin fix of the succeeding leg. + + +# Other Fields + +- `Vnav`: Angle from IAF to MAP. Useful for RNAV. \ No newline at end of file diff --git a/browser/src/App.tsx b/browser/src/App.tsx index ebdb5a6..254f6e6 100644 --- a/browser/src/App.tsx +++ b/browser/src/App.tsx @@ -24,7 +24,7 @@ function App() { return ( <> - {transitions.length === 0 ? ( + {!user ? (
{!initialized &&
Loading...
} @@ -38,7 +38,27 @@ function App() { )} - {user && ( +
+ ) : ( +
+ {selectedAirport && selectedRunway && selectedTerminal ? ( + { + setSelectedTerminal(undefined); + setSelectedChart(undefined); + setTransitions([]); + setSelectedTransition(undefined); + }} + /> + ) : ( { - const _transitions = selectedTransitions.map((transition) => ({ - name: transition, - data: parser.parse(selectedRunway!, transition), - })); + let _transitions = selectedTransitions + .map((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); setSelectedTransition(_transitions[0]); setSelectedChart(undefined); }} /> )} -
- ) : ( -
- {transitions.length > 0 && selectedAirport && selectedTerminal ? ( - <> - { - setSelectedTerminal(undefined); - setTransitions([]); - }} - /> - - - ) : ( -

Error

- )} +
)} diff --git a/browser/src/components/Loader.tsx b/browser/src/components/Loader.tsx new file mode 100644 index 0000000..3ff532c --- /dev/null +++ b/browser/src/components/Loader.tsx @@ -0,0 +1,22 @@ +import type { FC } from 'react'; + +interface LoaderProps { + size?: 0.5 | 1 | 2 | 3; +} + +export const Loader: FC = ({ 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 ( +
+ +
+ ); +}; diff --git a/browser/src/components/Map.tsx b/browser/src/components/Map.tsx index 218b3a9..cea15a7 100644 --- a/browser/src/components/Map.tsx +++ b/browser/src/components/Map.tsx @@ -1,22 +1,26 @@ import { default as L } from 'leaflet'; 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'; interface MapProps { - airport: Airport; + airport: Airport | undefined; chart: Chart | undefined; - procedure: Procedure | undefined; + procedures: Procedure[]; } -export const Map: FC = ({ airport, chart, procedure }) => { +export const Map: FC = ({ airport, chart, procedures }) => { const mapRef = createRef(); const imageRef = createRef(); + useEffect(() => { + if (airport) mapRef.current?.flyTo([airport?.Latitude, airport?.Longitude], 10, { animate: false }); + }, [airport]); + return ( { @@ -46,15 +50,15 @@ export const Map: FC = ({ airport, chart, procedure }) => { /> )} - {procedure && ( - <> + {procedures.map((procedure) => ( + ({ - color: '#ff00ff', + color: properties.isMAP ? '#00ffff' : '#ff00ff', stroke: true, - weight: 5, + weight: properties.isMAP ? 2.5 : 5, opacity: 1, dashArray: properties.isManual ? '20, 20' : undefined, })} @@ -71,12 +75,22 @@ export const Map: FC = ({ airport, chart, procedure }) => { weight: 3, }} 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, { shape: 'triangle', radius: 6, }); - if (properties.isIntersection) return L.circleMarker(latlng, { radius: 6 }); return L.shapeMarker(latlng, { shape: 'star-4', @@ -98,8 +112,8 @@ export const Map: FC = ({ airport, chart, procedure }) => { }} filter={(feature) => feature.geometry.type === 'Point'} /> - - )} + + ))} ); }; diff --git a/browser/src/components/ProcedureSelect.tsx b/browser/src/components/ProcedureSelect.tsx index f840689..8742f95 100644 --- a/browser/src/components/ProcedureSelect.tsx +++ b/browser/src/components/ProcedureSelect.tsx @@ -1,5 +1,6 @@ import { createRef, useMemo, useState, type Dispatch, type FC, type SetStateAction } from 'react'; import Parser from '../parser/parser'; +import { Loader } from './Loader'; const parser = await Parser.instance(); @@ -24,26 +25,32 @@ export const ProcedureSelect: FC = ({ }) => { const inputRef = createRef(); const [error, setError] = useState(); + const [inFlight, setInFlight] = useState(); const runways = useMemo( () => parser.runways.filter(({ AirportID }) => AirportID === selectedAirport?.ID), [selectedAirport] ); - const terminals = useMemo( - () => - parser.terminals.filter( - ({ AirportID, RwyID }) => AirportID === selectedAirport?.ID && (!RwyID || RwyID === selectedRunway?.ID) - ), - [selectedAirport, selectedRunway] - ); + const terminals = useMemo(() => { + const _terminals = parser.terminals.filter( + ({ AirportID, RwyID }) => AirportID === selectedAirport?.ID && (!RwyID || RwyID === selectedRunway?.ID) + ); + + 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 ( -
+
{selectedAirport && ( )} - {!selectedAirport && ( -
-

Enter Airport ICAO

- { - if (e.target.value.length <= 4) e.target.value = e.target.value.toUpperCase(); - else e.target.value = e.target.value.slice(0, 4); - }} - > - - {error && {error}} -
- )} - {selectedAirport && !selectedRunway && ( -
-

Select Runway

- {runways.map((runway) => ( +
+ {!selectedAirport && ( +
+

Enter Airport ICAO

+ { + if (e.target.value.length <= 4) e.target.value = e.target.value.toUpperCase(); + else e.target.value = e.target.value.slice(0, 4); + }} + > - ))} -
- )} - {selectedAirport && selectedRunway && !selectedTerminal && ( -
-

Select Procedure

- {terminals.map((terminal) => ( - - ))} -
- )} + {error && {error}} +
+ )} + + {selectedAirport && !selectedRunway && ( +
+

Select Runway

+ {runways.map((runway) => ( + + ))} +
+ )} + + {selectedAirport && selectedRunway && !selectedTerminal && ( +
+

Select Procedure

+ +
+
Arrivals
+ {terminals.star.map((terminal) => ( + + ))} +
+
+
Departures
+ {terminals.sid.map((terminal) => ( + + ))} +
+
+
Approaches
+ {terminals.iap.map((terminal) => ( + + ))} +
+
+ )} +
); }; diff --git a/browser/src/components/Sidebar.tsx b/browser/src/components/Sidebar.tsx index d544896..eba86b6 100644 --- a/browser/src/components/Sidebar.tsx +++ b/browser/src/components/Sidebar.tsx @@ -1,11 +1,13 @@ import BrowserImageManipulation from 'browser-image-manipulation'; import L from 'leaflet'; import type { Chart as NGChart } from 'navigraph/charts'; -import { useEffect, useState, type Dispatch, type FC, type SetStateAction } from 'react'; +import { useCallback, useEffect, useState, type Dispatch, type FC, type SetStateAction } from 'react'; import { charts } from '../lib/navigraph'; +import { Loader } from './Loader'; interface SidebarProps { airport: Airport; + runway: Runway; terminal: Terminal; transitions: Procedure[]; transition: Procedure | undefined; @@ -17,6 +19,7 @@ interface SidebarProps { export const Sidebar: FC = ({ airport, + runway, terminal, transitions, transition, @@ -25,14 +28,83 @@ export const Sidebar: FC = ({ setChart, backAction, }) => { - const [chartIndex, setChartIndex] = useState([]); + const [inFlight, setInFlight] = useState(false); + const [chartIndex, setChartIndex] = useState<{ + sid: NGChart[]; + star: NGChart[]; + iap: NGChart[]; + }>({ star: [], sid: [], iap: [] }); useEffect(() => { + setInFlight(true); (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]); + 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 (
))}
-
Charts
- {chartIndex - .filter((_chart) => _chart.is_georeferenced) - .map((_chart) => ( - + ))} +
- 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(); +
+
Departures
+ {chartIndex.sid + .filter((_chart) => _chart.is_georeferenced) + .map((_chart) => ( + + ))} +
- 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.index_number} -
- {_chart.name} - - ))} +
+
Approaches
+ {chartIndex.iap + .filter((_chart) => _chart.is_georeferenced) + .map((_chart) => ( + + ))} +
diff --git a/browser/src/parser/parser.ts b/browser/src/parser/parser.ts index 12f43d9..194d4c5 100644 --- a/browser/src/parser/parser.ts +++ b/browser/src/parser/parser.ts @@ -128,31 +128,32 @@ class Parser { name: runway.Ident, }); let lastCourse = runway.TrueHeading; + const legOptions = { isMAP: false }; const procedure = this._procedures.filter( ({ Transition }) => !Transition || Transition === transition || Transition === 'ALL' ); // Main for (let index = 0; index < procedure.length; index++) { - //lastCourse = runway.TrueHeading; const leg = procedure[index]; const previousFix = navFixes.at(-1)!; + legOptions.isMAP ||= previousFix.IsMAP ?? false; const waypoint = this.waypoints.find(({ ID }) => ID === leg.WptID); switch (leg.TrackCode) { case 'AF': { const [fixToAdd, lineToAdd] = TerminatorsAF(leg as AFTerminalEntry, previousFix, waypoint); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'CA': { const [fixToAdd, lineToAdd] = TerminatorsCA(leg as CATerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'CD': { const [fixToAdd, lineToAdd] = TerminatorsCD(leg as CDTerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'CF': { @@ -162,7 +163,7 @@ class Parser { lastCourse, waypoint ); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'CI': { @@ -172,37 +173,37 @@ class Parser { { ...previousFix }, // COPY lastCourse ); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'CR': { const [fixToAdd, lineToAdd] = TerminatorsCR(leg as CRTerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'DF': { const [fixToAdd, lineToAdd] = TerminatorsDF(leg as DFTerminalEntry, previousFix, lastCourse, waypoint); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'FA': { const [fixToAdd, lineToAdd] = TerminatorsFA(leg as FATerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'FC': { const [fixToAdd, lineToAdd] = TerminatorsFC(leg as FCTerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'FD': { const [fixToAdd, lineToAdd] = TerminatorsFD(leg as FDTerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'FM': { const [fixToAdd, lineToAdd] = TerminatorsFM(leg as FMTerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd, { isManual: true }); + update(fixToAdd, lineToAdd, { isManual: true, ...legOptions }); // Make overfly navFixes.at(-1)!.isFlyOver = true; break; @@ -231,7 +232,7 @@ class Parser { lastCourse = (leg as RFTerminalEntry).Course?.toTrue(fixToAdd); } if (lineToAdd) { - lineSegments.push({ line: lineToAdd }); + lineSegments.push({ line: lineToAdd, ...legOptions }); } break; } @@ -242,17 +243,17 @@ class Parser { lastCourse, waypoint ); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'VA': { const [fixToAdd, lineToAdd] = TerminatorsVA(leg as VATerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'VD': { const [fixToAdd, lineToAdd] = TerminatorsVD(leg as VDTerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'VI': { @@ -262,19 +263,19 @@ class Parser { { ...previousFix }, // COPY lastCourse ); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } case 'VM': { const [fixToAdd, lineToAdd] = TerminatorsVM(leg as VMTerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd, { isManual: true }); + update(fixToAdd, lineToAdd, { isManual: true, ...legOptions }); // Make overfly navFixes.at(-1)!.isFlyOver = true; break; } case 'VR': { const [fixToAdd, lineToAdd] = TerminatorsVR(leg as VRTerminalEntry, previousFix, lastCourse); - update(fixToAdd, lineToAdd); + update(fixToAdd, lineToAdd, legOptions); break; } default: diff --git a/browser/src/parser/pathGenerators/generateAFArc.ts b/browser/src/parser/pathGenerators/generateAFArc.ts index 85c2211..cf92566 100644 --- a/browser/src/parser/pathGenerators/generateAFArc.ts +++ b/browser/src/parser/pathGenerators/generateAFArc.ts @@ -6,7 +6,7 @@ import computeDestinationPoint from 'geolib/es/computeDestinationPoint'; * @param start Arc origin point * @param center Arc center point * @param radius Arc radius in nmi - * @param turnDir + * @param turnDir Turn direction * @returns Line segments */ export const generateAFArc = ( diff --git a/browser/src/parser/pathGenerators/generateTangentArc.ts b/browser/src/parser/pathGenerators/generateTangentArc.ts index 3508dae..d49c0c7 100644 --- a/browser/src/parser/pathGenerators/generateTangentArc.ts +++ b/browser/src/parser/pathGenerators/generateTangentArc.ts @@ -56,14 +56,15 @@ export const generateTangentArc = ( } // Generate arc + let arcRad = 0; let arcCenter = computeIntersection( start, crsOrthogonalOnOrigin, intcArcOnCrsIntoEndpoint, crsOrthogonalOnEndpoint ); - let arcRad = 0; - if (arcCenter) arcRad = getPreciseDistance(arcCenter, start); + if (Math.abs(crsOrthogonalOnEndpoint - crsOrthogonalOnOrigin) <= 0.1 && arcCenter) + arcRad = getPreciseDistance(arcCenter, start); else { arcRad = getPreciseDistance(start, end) / 2; arcCenter = computeDestinationPoint(start, arcRad, crsOrthogonalOnOrigin); diff --git a/browser/src/parser/terminators/AF.ts b/browser/src/parser/terminators/AF.ts index 5575793..21889a3 100644 --- a/browser/src/parser/terminators/AF.ts +++ b/browser/src/parser/terminators/AF.ts @@ -7,6 +7,11 @@ export const TerminatorsAF = ( previousFix: NavFix, waypoint?: Waypoint ): [NavFix?, LineSegment[]?] => { + const navaid = { + latitude: leg.NavLat, + longitude: leg.NavLon, + }; + const targetFix: NavFix = { latitude: leg.WptLat, longitude: leg.WptLon, @@ -16,27 +21,13 @@ export const TerminatorsAF = ( speed: computeSpeed(leg, previousFix), speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; - const arcEndCrs = getGreatCircleBearing( - { - latitude: leg.NavLat, - longitude: leg.NavLon, - }, - { - latitude: leg.WptLat, - longitude: leg.WptLon, - } - ); + const arcEndCrs = getGreatCircleBearing(navaid, targetFix); - const line = generateAFArc( - arcEndCrs, - leg.Course.toTrue({ latitude: leg.NavLat, longitude: leg.NavLon }), - previousFix, - { latitude: leg.NavLat, longitude: leg.NavLon }, - leg.NavDist, - leg.TurnDir - ); + const line = generateAFArc(arcEndCrs, leg.Course.toTrue(navaid), previousFix, navaid, leg.NavDist, leg.TurnDir); return [targetFix, line]; }; diff --git a/browser/src/parser/terminators/CA.ts b/browser/src/parser/terminators/CA.ts index 6b1510a..a9b4bd3 100644 --- a/browser/src/parser/terminators/CA.ts +++ b/browser/src/parser/terminators/CA.ts @@ -31,6 +31,8 @@ export const TerminatorsCA = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/CD.ts b/browser/src/parser/terminators/CD.ts index 5056874..b33bef0 100644 --- a/browser/src/parser/terminators/CD.ts +++ b/browser/src/parser/terminators/CD.ts @@ -44,6 +44,8 @@ export const TerminatorsCD = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/CF.ts b/browser/src/parser/terminators/CF.ts index b4900cb..68aab77 100644 --- a/browser/src/parser/terminators/CF.ts +++ b/browser/src/parser/terminators/CF.ts @@ -3,7 +3,6 @@ import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import getPreciseDistance from 'geolib/es/getPreciseDistance'; import Parser from '../parser'; import { generateTangentArc } from '../pathGenerators/generateTangentArc'; -import { computeIntersection } from '../utils/computeIntersection'; import { computeSpeed } from '../utils/computeSpeed'; import { computeTurnRate } from '../utils/computeTurnRate'; @@ -26,6 +25,8 @@ export const TerminatorsCF = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; const crsToIntercept = leg.Course.toTrue(targetFix); @@ -38,74 +39,6 @@ export const TerminatorsCF = ( 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; if (arc1 && arc1.length > 1) { const endCrs = getGreatCircleBearing( @@ -124,10 +57,61 @@ export const TerminatorsCF = ( ); 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([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/CI.ts b/browser/src/parser/terminators/CI.ts index a1bc65c..db07a76 100644 --- a/browser/src/parser/terminators/CI.ts +++ b/browser/src/parser/terminators/CI.ts @@ -68,6 +68,8 @@ export const TerminatorsCI = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([interceptFix.longitude, interceptFix.latitude]); diff --git a/browser/src/parser/terminators/CR.ts b/browser/src/parser/terminators/CR.ts index 7353cdd..3486961 100644 --- a/browser/src/parser/terminators/CR.ts +++ b/browser/src/parser/terminators/CR.ts @@ -27,6 +27,8 @@ export const TerminatorsCR = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([interceptFix.longitude, interceptFix.latitude]); diff --git a/browser/src/parser/terminators/DF.ts b/browser/src/parser/terminators/DF.ts index d2f6acc..47587ad 100644 --- a/browser/src/parser/terminators/DF.ts +++ b/browser/src/parser/terminators/DF.ts @@ -25,6 +25,8 @@ export const TerminatorsDF = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; let crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix); diff --git a/browser/src/parser/terminators/FA.ts b/browser/src/parser/terminators/FA.ts index fe87314..e9e2f47 100644 --- a/browser/src/parser/terminators/FA.ts +++ b/browser/src/parser/terminators/FA.ts @@ -35,6 +35,8 @@ export const TerminatorsFA = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/FC.ts b/browser/src/parser/terminators/FC.ts index d499f32..87c8088 100644 --- a/browser/src/parser/terminators/FC.ts +++ b/browser/src/parser/terminators/FC.ts @@ -87,6 +87,8 @@ export const TerminatorsFC = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/FD.ts b/browser/src/parser/terminators/FD.ts index c9cfe81..8b94420 100644 --- a/browser/src/parser/terminators/FD.ts +++ b/browser/src/parser/terminators/FD.ts @@ -48,6 +48,8 @@ export const TerminatorsFD = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/IF.ts b/browser/src/parser/terminators/IF.ts index 18941af..66d8e74 100644 --- a/browser/src/parser/terminators/IF.ts +++ b/browser/src/parser/terminators/IF.ts @@ -10,6 +10,8 @@ export const TerminatorsIF = (leg: IFTerminalEntry, waypoint?: Waypoint): NavFix speed: leg.SpeedLimit ? leg.SpeedLimit : Parser.AC_SPEED, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; return targetFix; diff --git a/browser/src/parser/terminators/RF.ts b/browser/src/parser/terminators/RF.ts index 8fa04c9..f29d3bb 100644 --- a/browser/src/parser/terminators/RF.ts +++ b/browser/src/parser/terminators/RF.ts @@ -17,6 +17,8 @@ export const TerminatorsRF = ( speed: computeSpeed(leg, previousFix), speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; const line = generateRFArc( diff --git a/browser/src/parser/terminators/TF.ts b/browser/src/parser/terminators/TF.ts index 4f169a4..118e8b8 100644 --- a/browser/src/parser/terminators/TF.ts +++ b/browser/src/parser/terminators/TF.ts @@ -3,7 +3,6 @@ import getGreatCircleBearing from 'geolib/es/getGreatCircleBearing'; import getPreciseDistance from 'geolib/es/getPreciseDistance'; import Parser from '../parser'; import { generateTangentArc } from '../pathGenerators/generateTangentArc'; -import { computeIntersection } from '../utils/computeIntersection'; import { computeSpeed } from '../utils/computeSpeed'; import { computeTurnRate } from '../utils/computeTurnRate'; @@ -25,6 +24,8 @@ export const TerminatorsTF = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; const crsIntoEndpoint = getGreatCircleBearing(previousFix, targetFix); @@ -37,69 +38,6 @@ export const TerminatorsTF = ( 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; if (arc1 && arc1.length > 1) { const endCrs = getGreatCircleBearing( @@ -118,10 +56,61 @@ export const TerminatorsTF = ( ); 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([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/VA.ts b/browser/src/parser/terminators/VA.ts index 8254d50..2463009 100644 --- a/browser/src/parser/terminators/VA.ts +++ b/browser/src/parser/terminators/VA.ts @@ -32,6 +32,8 @@ export const TerminatorsVA = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/VD.ts b/browser/src/parser/terminators/VD.ts index 3c45020..2a67b88 100644 --- a/browser/src/parser/terminators/VD.ts +++ b/browser/src/parser/terminators/VD.ts @@ -45,6 +45,8 @@ export const TerminatorsVD = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([targetFix.longitude, targetFix.latitude]); diff --git a/browser/src/parser/terminators/VI.ts b/browser/src/parser/terminators/VI.ts index c2c4c8b..466eb58 100644 --- a/browser/src/parser/terminators/VI.ts +++ b/browser/src/parser/terminators/VI.ts @@ -69,6 +69,8 @@ export const TerminatorsVI = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([interceptFix.longitude, interceptFix.latitude]); diff --git a/browser/src/parser/terminators/VR.ts b/browser/src/parser/terminators/VR.ts index 94f14a8..7694199 100644 --- a/browser/src/parser/terminators/VR.ts +++ b/browser/src/parser/terminators/VR.ts @@ -28,6 +28,8 @@ export const TerminatorsVR = ( speed: speed, speedConstraint: leg.SpeedLimit, altitudeConstraint: leg.Alt, + IsFAF: leg.IsFAF, + IsMAP: leg.IsMAP, }; line.push([interceptFix.longitude, interceptFix.latitude]); diff --git a/browser/src/style.css b/browser/src/style.css index d4b5078..73b707b 100644 --- a/browser/src/style.css +++ b/browser/src/style.css @@ -1 +1,15 @@ @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); + } + } +} diff --git a/browser/src/types/navdata.d.ts b/browser/src/types/navdata.d.ts index e650321..290adab 100644 --- a/browser/src/types/navdata.d.ts +++ b/browser/src/types/navdata.d.ts @@ -85,9 +85,10 @@ export declare global { speed?: number; name?: string; isFlyOver?: boolean; - 'marker-color'?: string; altitudeConstraint?: string; speedConstraint?: number; + IsFAF?: boolean; + IsMAP?: boolean; // For map isIntersection?: boolean; };