Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(fms): proper xD leg terminations, improved paths, and airport as fix #9781

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions fbw-a32nx/src/systems/fmgc/src/guidance/geometry/GeometryFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { BaseFlightPlan } from '@fmgc/flightplanning/plans/BaseFlightPlan';
import { Leg } from '@fmgc/guidance/lnav/legs/Leg';
import { Transition } from '@fmgc/guidance/lnav/Transition';
import { FlightPlanElement, FlightPlanLeg } from '@fmgc/flightplanning/legs/FlightPlanLeg';
import { LegType } from '@flybywiresim/fbw-sdk';
import { isVhfNavaid, LegType } from '@flybywiresim/fbw-sdk';
import { TFLeg } from '@fmgc/guidance/lnav/legs/TF';
import { SegmentType } from '@fmgc/flightplanning/FlightPlanSegment';
import { IFLeg } from '@fmgc/guidance/lnav/legs/IF';
Expand All @@ -24,6 +24,7 @@ import { XFLeg } from '@fmgc/guidance/lnav/legs/XF';
import { VMLeg } from '@fmgc/guidance/lnav/legs/VM';
import { RFLeg } from '@fmgc/guidance/lnav/legs/RF';
import { CRLeg } from '@fmgc/guidance/lnav/legs/CR';
import { FCLeg } from '@fmgc/guidance/lnav/legs/FC';
import { FDLeg } from '@fmgc/guidance/lnav/legs/FD';
import { CDLeg } from '@fmgc/guidance/lnav/legs/CD';
import { PILeg } from '@fmgc/guidance/lnav/legs/PI';
Expand Down Expand Up @@ -270,13 +271,15 @@ function geometryLegFromFlightPlanLeg(
}
case LegType.CA:
case LegType.VA: {
// TODO FA, VA legs in geometry
const altitude = flightPlanLeg.definition.altitude1;

return new CALeg(trueCourse, altitude, metadata, SegmentType.Departure);
}
case LegType.CD:
case LegType.VD: // TODO FA, VA legs in geometry
case LegType.VD:
if (!isVhfNavaid(recommendedNavaid)) {
throw new Error('[FMS/Geometry] Cannot create a CD or VD leg with invalid recommended navaid');
}
return new CDLeg(trueCourse, length, recommendedNavaid, metadata, SegmentType.Departure);
case LegType.CF:
return new CFLeg(waypoint, trueCourse, length, metadata, SegmentType.Departure);
Expand Down Expand Up @@ -309,15 +312,12 @@ function geometryLegFromFlightPlanLeg(
case LegType.FA:
return new FALeg(waypoint, trueCourse, flightPlanLeg.definition.altitude1, metadata, SegmentType.Departure);
case LegType.FC:
return new FCLeg(trueCourse, length, waypoint, metadata, SegmentType.Departure);
case LegType.FD:
return new FDLeg(
trueCourse,
length,
waypoint,
legType === LegType.FC ? waypoint : recommendedNavaid,
metadata,
SegmentType.Departure,
);
if (!isVhfNavaid(recommendedNavaid)) {
throw new Error('[FMS/Geometry] Cannot create a FD leg with invalid recommended navaid');
}
return new FDLeg(trueCourse, length, waypoint, recommendedNavaid, metadata, SegmentType.Departure);
case LegType.FM:
return new FMLeg(flightPlanLeg.terminationWaypoint(), trueCourse, metadata, SegmentType.Departure);
case LegType.IF:
Expand Down
69 changes: 57 additions & 12 deletions fbw-a32nx/src/systems/fmgc/src/guidance/lnav/TransitionPicker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2021-2022 FlyByWire Simulations
// Copyright (c) 2021-2022, 2025 FlyByWire Simulations
// Copyright (c) 2021-2022 Synaptic Simulations
//
// SPDX-License-Identifier: GPL-3.0
Expand All @@ -23,6 +23,7 @@ import { AFLeg } from '@fmgc/guidance/lnav/legs/AF';
import { DmeArcTransition } from '@fmgc/guidance/lnav/transitions/DmeArcTransition';
import { PILeg } from '@fmgc/guidance/lnav/legs/PI';
import { CDLeg } from '@fmgc/guidance/lnav/legs/CD';
import { FCLeg } from '@fmgc/guidance/lnav/legs/FC';
import { FDLeg } from '@fmgc/guidance/lnav/legs/FD';
import { FMLeg } from '@fmgc/guidance/lnav/legs/FM';
import { FALeg } from '@fmgc/guidance/lnav/legs/FA';
Expand Down Expand Up @@ -54,6 +55,9 @@ export class TransitionPicker {
if (from instanceof FALeg) {
return TransitionPicker.fromFA(from, to);
}
if (from instanceof FCLeg) {
return TransitionPicker.fromFC(from, to);
}
if (from instanceof FDLeg) {
return TransitionPicker.fromFD(from, to);
}
Expand Down Expand Up @@ -92,7 +96,7 @@ export class TransitionPicker {
if (to instanceof DFLeg) {
return new DirectToFixTransition(from, to);
}
if (to instanceof FALeg || to instanceof FDLeg || to instanceof FMLeg) {
if (to instanceof FALeg || to instanceof FCLeg || to instanceof FDLeg || to instanceof FMLeg) {
return new PathCaptureTransition(from, to);
}
if (to instanceof CILeg) {
Expand Down Expand Up @@ -137,7 +141,7 @@ export class TransitionPicker {
if (to instanceof DFLeg) {
return new DirectToFixTransition(from, to);
}
if (to instanceof FALeg || to instanceof FDLeg || to instanceof FMLeg) {
if (to instanceof FALeg || to instanceof FCLeg || to instanceof FDLeg || to instanceof FMLeg) {
return new PathCaptureTransition(from, to);
}
if (to instanceof VMLeg) {
Expand All @@ -164,7 +168,7 @@ export class TransitionPicker {
if (to instanceof CRLeg) {
return new CourseCaptureTransition(from, to);
}
if (to instanceof FDLeg) {
if (to instanceof FCLeg || to instanceof FDLeg) {
// FIXME here we might wanna do a DmeArcTransition; need to check ARINC 424 and IRL FMS to see if this would actually happen
// (waypoint would need to lie on DME arc)
return new PathCaptureTransition(from, to);
Expand Down Expand Up @@ -210,7 +214,7 @@ export class TransitionPicker {
if (to instanceof FALeg) {
return new FixedRadiusTransition(from, to);
}
if (to instanceof FDLeg) {
if (to instanceof FCLeg || to instanceof FDLeg) {
return new PathCaptureTransition(from, to);
}
if (to instanceof FMLeg) {
Expand Down Expand Up @@ -249,7 +253,7 @@ export class TransitionPicker {
if (to instanceof CFLeg) {
return new FixedRadiusTransition(from, to);
}
if (to instanceof FDLeg) {
if (to instanceof FCLeg || to instanceof FDLeg) {
return new PathCaptureTransition(from, to);
}
if (to instanceof FALeg || to instanceof FMLeg) {
Expand Down Expand Up @@ -286,7 +290,15 @@ export class TransitionPicker {
if (to instanceof FALeg) {
return new FixedRadiusTransition(from, to);
}
if (to instanceof FDLeg) {
if (to instanceof FCLeg || to instanceof FDLeg) {
const fromLegWaypointID = from.metadata.flightPlanLegDefinition.waypoint.databaseId;
const toLegWaypointID = to.metadata.flightPlanLegDefinition.waypoint.databaseId;

// If the FC/FD leg starts at the same fix as the one on which the TF leg ends, we can use a fixed radius transition
// instead to get a cleaner turn
if (fromLegWaypointID === toLegWaypointID) {
return new FixedRadiusTransition(from, to);
}
return new PathCaptureTransition(from, to);
}
if (to instanceof FMLeg) {
Expand Down Expand Up @@ -334,7 +346,7 @@ export class TransitionPicker {
if (to instanceof DFLeg) {
return new DirectToFixTransition(from, to);
}
if (to instanceof FALeg || to instanceof FDLeg || to instanceof FMLeg) {
if (to instanceof FALeg || to instanceof FCLeg || to instanceof FDLeg || to instanceof FMLeg) {
return new PathCaptureTransition(from, to);
}
if (to instanceof TFLeg) {
Expand Down Expand Up @@ -375,7 +387,7 @@ export class TransitionPicker {
if (to instanceof CILeg) {
return new CourseCaptureTransition(from, to);
}
if (to instanceof FALeg || to instanceof FDLeg || to instanceof FMLeg) {
if (to instanceof FALeg || to instanceof FCLeg || to instanceof FDLeg || to instanceof FMLeg) {
return new PathCaptureTransition(from, to);
}
if (to instanceof HALeg || to instanceof HFLeg || to instanceof HMLeg) {
Expand Down Expand Up @@ -406,11 +418,11 @@ export class TransitionPicker {
if (to instanceof DFLeg) {
return new DirectToFixTransition(from, to);
}
if (to instanceof FDLeg) {
if (to instanceof FCLeg || to instanceof FDLeg) {
const fromLegWaypointID = from.metadata.flightPlanLegDefinition.waypoint.databaseId;
const toLegWaypointID = to.metadata.flightPlanLegDefinition.waypoint.databaseId;

// If the FD leg starts at the same fix as the one on which the TF leg ends, we can use a fixed radius transition
// If the FC/FD leg starts at the same fix as the one on which the TF leg ends, we can use a fixed radius transition
// instead to get a cleaner turn
if (fromLegWaypointID === toLegWaypointID) {
return new FixedRadiusTransition(from, to);
Expand Down Expand Up @@ -461,7 +473,7 @@ export class TransitionPicker {
if (to instanceof DFLeg) {
return new DirectToFixTransition(from, to);
}
if (to instanceof FALeg || to instanceof FDLeg) {
if (to instanceof FALeg || to instanceof FCLeg || to instanceof FDLeg) {
return new PathCaptureTransition(from, to);
}
if (to instanceof FMLeg) {
Expand Down Expand Up @@ -510,6 +522,39 @@ export class TransitionPicker {
}
}

private static fromFC(from: FCLeg, to: Leg) {
if (to instanceof CALeg) {
return new CourseCaptureTransition(from, to);
}
if (to instanceof CDLeg) {
return new CourseCaptureTransition(from, to);
}
if (to instanceof CFLeg) {
return new PathCaptureTransition(from, to);
}
if (to instanceof CILeg) {
return new CourseCaptureTransition(from, to);
}
if (to instanceof CRLeg) {
return new CourseCaptureTransition(from, to);
}
if (to instanceof DFLeg) {
return new DirectToFixTransition(from, to);
}
if (to instanceof FMLeg) {
return new PathCaptureTransition(from, to);
}
if (to instanceof VMLeg) {
return new CourseCaptureTransition(from, to);
}

if (LnavConfig.DEBUG_GEOMETRY) {
console.error(`Illegal sequence FCLeg -> ${to.constructor.name}`);
}

return null;
}

private static fromFD(from: FDLeg, to: Leg) {
if (to instanceof AFLeg) {
return new DmeArcTransition(from, to);
Expand Down
10 changes: 6 additions & 4 deletions fbw-a32nx/src/systems/fmgc/src/guidance/lnav/legs/CD.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { Leg } from '@fmgc/guidance/lnav/legs/Leg';
import { PathVector, PathVectorType } from '@fmgc/guidance/lnav/PathVector';
import { SegmentType } from '@fmgc/flightplanning/FlightPlanSegment';
import { NdbNavaid, VhfNavaid, Waypoint } from '@flybywiresim/fbw-sdk';
import { VhfNavaid, Waypoint } from '@flybywiresim/fbw-sdk';
import { Coordinates, distanceTo, firstSmallCircleIntersection } from 'msfs-geo';
import { GuidanceParameters } from '@fmgc/guidance/ControlLaws';
import { LnavConfig } from '@fmgc/guidance/LnavConfig';
Expand All @@ -22,10 +22,12 @@ export class CDLeg extends Leg {

pbdPoint: Coordinates;

private readonly dmeLocation = this.origin.dmeLocation ?? this.origin.location;

constructor(
private readonly course: DegreesTrue,
private readonly dmeDistance: NauticalMiles,
private readonly origin: Waypoint | VhfNavaid | NdbNavaid,
private readonly origin: VhfNavaid,
public readonly metadata: Readonly<LegMetadata>,
segment: SegmentType,
) {
Expand All @@ -46,7 +48,7 @@ export class CDLeg extends Leg {
return this.inboundGuidable.getPathEndPoint();
}

return this.origin.location;
return this.dmeLocation;
}

getPathEndPoint(): Coordinates | undefined {
Expand All @@ -57,7 +59,7 @@ export class CDLeg extends Leg {
this.predictedPath.length = 0;

const intersect = firstSmallCircleIntersection(
this.origin.location,
this.dmeLocation,
this.dmeDistance,
this.getPathStartPoint(),
this.course,
Expand Down
134 changes: 134 additions & 0 deletions fbw-a32nx/src/systems/fmgc/src/guidance/lnav/legs/FC.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) 2021-2022, 2025 FlyByWire Simulations
// Copyright (c) 2021-2022 Synaptic Simulations
//
// SPDX-License-Identifier: GPL-3.0

import { Leg } from '@fmgc/guidance/lnav/legs/Leg';
import { PathVector, PathVectorType } from '@fmgc/guidance/lnav/PathVector';
import { SegmentType } from '@fmgc/flightplanning/FlightPlanSegment';
import { Fix, Waypoint } from '@flybywiresim/fbw-sdk';
import { Coordinates, distanceTo, placeBearingDistance } from 'msfs-geo';
import { GuidanceParameters } from '@fmgc/guidance/ControlLaws';
import { LnavConfig } from '@fmgc/guidance/LnavConfig';
import { LegMetadata } from './index';
import { courseToFixDistanceToGo, fixToFixGuidance } from '../CommonGeometry';

export class FCLeg extends Leg {
/** @inheritdoc */
public predictedPath: PathVector[];

/** @inheritdoc */
public inboundCourse = this.course;
/** @inheritdoc */
public outboundCourse = this.course;

// not actually an "intercepted", but that prop is required by transitions
/** The endpoint of the path, not considering the outbound transition. */
public intercept: Coordinates;

/**
* Ctor.
* @param course True course of the leg in degrees.
* @param legLength Length of the leg from the fix in nautical miles.
* @param fix The fix to start from.
* @param metadata Additional leg metadata.
* @param segment The segment this leg belongs to.
*/
constructor(
private readonly course: number,
private readonly legLength: number,
private readonly fix: Fix,
public readonly metadata: Readonly<LegMetadata>,
public readonly segment: SegmentType,
) {
super();

// FC legs can be statically computed the first time

this.intercept = placeBearingDistance(fix.location, this.course, this.legLength);

this.predictedPath = [
{
type: PathVectorType.Line,
startPoint: this.getPathStartPoint(),
endPoint: this.getPathEndPoint(),
},
];

if (LnavConfig.DEBUG_PREDICTED_PATH) {
this.predictedPath.push(
{
type: PathVectorType.DebugPoint,
startPoint: this.getPathStartPoint(),
annotation: 'FC START',
},
{
type: PathVectorType.DebugPoint,
startPoint: this.getPathEndPoint(),
annotation: 'FC END',
},
);
}

this.isComputed = true;
}

get terminationWaypoint(): Waypoint | Coordinates | undefined {
return this.intercept;
}

getPathStartPoint(): Coordinates | undefined {
if (this.inboundGuidable && this.inboundGuidable.isComputed) {
return this.inboundGuidable.getPathEndPoint();
}

return this.fix.location;
}

getPathEndPoint(): Coordinates | undefined {
return this.intercept;
}

recomputeWithParameters(_isActive: boolean, _tas: Knots, _gs: Knots, _ppos: Coordinates, _trueTrack: DegreesTrue) {
this.predictedPath[0].startPoint = this.getPathStartPoint();

if (LnavConfig.DEBUG_PREDICTED_PATH) {
this.predictedPath[1].startPoint = this.getPathStartPoint();
}

this.isComputed = true;
}

get distanceToTermination(): NauticalMiles {
const startPoint = this.getPathStartPoint();

return distanceTo(startPoint, this.intercept);
}

isAbeam(ppos: Coordinates): boolean {
const dtg = this.getDistanceToGo(ppos);

return dtg >= 0 && dtg <= this.distance;
}

getDistanceToGo(ppos: Coordinates): NauticalMiles | undefined {
return courseToFixDistanceToGo(ppos, this.course, this.intercept);
}

getGuidanceParameters(
ppos: Coordinates,
trueTrack: Degrees,
_tas: Knots,
_gs: Knots,
): GuidanceParameters | undefined {
return fixToFixGuidance(ppos, trueTrack, this.getPathStartPoint(), this.intercept);
}

getNominalRollAngle(_gs: MetresPerSecond): Degrees | undefined {
return 0;
}

get repr(): string {
return `FC(${this.legLength.toFixed(1)}NM, ${this.course.toFixed(1)})`;
}
}
Loading
Loading