diff --git a/fbw-a32nx/src/systems/fmgc/src/flightplanning/FlightPlanInterface.ts b/fbw-a32nx/src/systems/fmgc/src/flightplanning/FlightPlanInterface.ts
index 33767f018e5..295a23289e9 100644
--- a/fbw-a32nx/src/systems/fmgc/src/flightplanning/FlightPlanInterface.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/flightplanning/FlightPlanInterface.ts
@@ -4,7 +4,7 @@
import { Fix, Waypoint } from '@flybywiresim/fbw-sdk';
import { Coordinates, Degrees } from 'msfs-geo';
-import { HoldData } from '@fmgc/flightplanning/data/flightplan';
+import { HoldData, OffsetData } from '@fmgc/flightplanning/data/flightplan';
import { FlightPlanLegDefinition } from '@fmgc/flightplanning/legs/FlightPlanLegDefinition';
import { FixInfoEntry } from '@fmgc/flightplanning/plans/FixInfo';
import { FlightPlan } from '@fmgc/flightplanning/plans/FlightPlan';
@@ -253,6 +253,23 @@ export interface FlightPlanInterface
;
+ /**
+ * OFFSET revision. Inserts or eidts an offset with a defined start and end point.
+ *
+ * @param startIndex the index of the leg to start the offset at
+ * @param endIndex the index of the leg to end the offset at
+ * @param desiredOffset the desired offset
+ * @param planIndex the flight plan to make the change on
+ * @param alternate whether to edit the plan's alternate flight plan
+ */
+ setOffsetParams(
+ startIndex: number,
+ endIndex: number,
+ desiredOffset: OffsetData,
+ planIndex: FlightPlanIndex,
+ alternate?: boolean,
+ ): Promise;
+
/**
* Reverts a hold parented to a leg to a computed hold.
*
diff --git a/fbw-a32nx/src/systems/fmgc/src/flightplanning/FlightPlanService.ts b/fbw-a32nx/src/systems/fmgc/src/flightplanning/FlightPlanService.ts
index aa950d59366..de78ad9fa3d 100644
--- a/fbw-a32nx/src/systems/fmgc/src/flightplanning/FlightPlanService.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/flightplanning/FlightPlanService.ts
@@ -11,7 +11,7 @@ import { NavigationDatabase } from '@fmgc/NavigationDatabase';
import { Coordinates, Degrees } from 'msfs-geo';
import { BitFlags, EventBus } from '@microsoft/msfs-sdk';
import { FixInfoEntry } from '@fmgc/flightplanning/plans/FixInfo';
-import { HoldData } from '@fmgc/flightplanning/data/flightplan';
+import { HoldData, OffsetData } from '@fmgc/flightplanning/data/flightplan';
import { FlightPlanLegDefinition } from '@fmgc/flightplanning/legs/FlightPlanLegDefinition';
import { FlightPlanInterface } from '@fmgc/flightplanning/FlightPlanInterface';
import { AltitudeConstraint } from '@fmgc/flightplanning/data/constraint';
@@ -461,6 +461,22 @@ export class FlightPlanService
{
+ const finalIndex = this.prepareDestructiveModification(planIndex);
+
+ const plan = alternate
+ ? this.flightPlanManager.get(finalIndex).alternateFlightPlan
+ : this.flightPlanManager.get(finalIndex);
+
+ plan.setOffsetParams(startIndex, endIndex, desiredOffset);
+ }
+
async setPilotEnteredAltitudeConstraintAt(
atIndex: number,
isDescentConstraint: boolean,
diff --git a/fbw-a32nx/src/systems/fmgc/src/flightplanning/data/flightplan.ts b/fbw-a32nx/src/systems/fmgc/src/flightplanning/data/flightplan.ts
index 88480f20746..38db081c3ab 100644
--- a/fbw-a32nx/src/systems/fmgc/src/flightplanning/data/flightplan.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/flightplanning/data/flightplan.ts
@@ -74,3 +74,18 @@ export interface StepData {
ident: string;
}
+
+export interface OffsetData {
+ interceptAngle?: number;
+
+ offsetDistance?: NauticalMiles;
+
+ offsetDirection?: TurnDirection;
+
+ offsetFlags: offsetFlags;
+}
+
+export enum offsetFlags {
+ Start = 0,
+ End = 1,
+}
diff --git a/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/FlightPlanLeg.ts b/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/FlightPlanLeg.ts
index a6e46ed99aa..63e8b270b3a 100644
--- a/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/FlightPlanLeg.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/FlightPlanLeg.ts
@@ -19,7 +19,7 @@ import { procedureLegIdentAndAnnotation } from '@fmgc/flightplanning/legs/Flight
import { WaypointFactory } from '@fmgc/flightplanning/waypoints/WaypointFactory';
import { FlightPlanSegment } from '@fmgc/flightplanning/segments/FlightPlanSegment';
import { EnrouteSegment } from '@fmgc/flightplanning/segments/EnrouteSegment';
-import { HoldData } from '@fmgc/flightplanning/data/flightplan';
+import { HoldData, OffsetData } from '@fmgc/flightplanning/data/flightplan';
import { CruiseStepEntry } from '@fmgc/flightplanning/CruiseStep';
import { WaypointConstraintType, AltitudeConstraint, SpeedConstraint } from '@fmgc/flightplanning/data/constraint';
import { HoldUtils } from '@fmgc/flightplanning/data/hold';
@@ -42,6 +42,7 @@ export interface SerializedFlightPlanLeg {
cruiseStep: CruiseStepEntry | undefined;
pilotEnteredAltitudeConstraint: AltitudeConstraint | undefined;
pilotEnteredSpeedConstraint: SpeedConstraint | undefined;
+ lateralOffset: OffsetData | undefined;
}
export enum FlightPlanLegFlags {
@@ -105,6 +106,8 @@ export class FlightPlanLeg implements ReadonlyFlightPlanLeg {
calculated: LegCalculations | undefined;
+ lateralOffset: OffsetData | undefined = undefined;
+
serialize(): SerializedFlightPlanLeg {
return {
ident: this.ident,
@@ -123,6 +126,7 @@ export class FlightPlanLeg implements ReadonlyFlightPlanLeg {
pilotEnteredSpeedConstraint: this.pilotEnteredSpeedConstraint
? JSON.parse(JSON.stringify(this.pilotEnteredSpeedConstraint))
: undefined,
+ lateralOffset: this.lateralOffset ? JSON.parse(JSON.stringify(this.lateralOffset)) : undefined,
};
}
@@ -143,6 +147,7 @@ export class FlightPlanLeg implements ReadonlyFlightPlanLeg {
leg.cruiseStep = serialized.cruiseStep;
leg.pilotEnteredAltitudeConstraint = serialized.pilotEnteredAltitudeConstraint;
leg.pilotEnteredSpeedConstraint = serialized.pilotEnteredSpeedConstraint;
+ leg.lateralOffset = serialized.lateralOffset;
return leg;
}
@@ -288,6 +293,7 @@ export class FlightPlanLeg implements ReadonlyFlightPlanLeg {
this.pilotEnteredSpeedConstraint = from.pilotEnteredSpeedConstraint;
this.constraintType = from.constraintType;
this.cruiseStep = from.cruiseStep;
+ this.lateralOffset = from.lateralOffset;
/**
* Don't copy holds. When we string the arrival to the upstream plan, the upstream plan may have a hold
* and the downstream leg doesn't, but the upstream leg is the one that's kept. In this case, we don't want to remove the hold
diff --git a/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/ReadonlyFlightPlanLeg.ts b/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/ReadonlyFlightPlanLeg.ts
index 9611b8c7687..c28458cd3dd 100644
--- a/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/ReadonlyFlightPlanLeg.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/ReadonlyFlightPlanLeg.ts
@@ -5,7 +5,7 @@
import { LegType } from '@flybywiresim/fbw-sdk';
import { FlightPlanSegment } from '@fmgc/flightplanning/segments/FlightPlanSegment';
import { FlightPlanLegDefinition } from '@fmgc/flightplanning/legs/FlightPlanLegDefinition';
-import { HoldData } from '@fmgc/flightplanning/data/flightplan';
+import { HoldData, OffsetData } from '@fmgc/flightplanning/data/flightplan';
import { WaypointConstraintType, AltitudeConstraint, SpeedConstraint } from '@fmgc/flightplanning/data/constraint';
import { CruiseStepEntry } from '@fmgc/flightplanning/CruiseStep';
@@ -39,6 +39,8 @@ export interface ReadonlyFlightPlanLeg {
readonly pilotEnteredAltitudeConstraint: AltitudeConstraint | undefined;
readonly pilotEnteredSpeedConstraint: SpeedConstraint | undefined;
+
+ readonly lateralOffset: OffsetData | undefined;
}
export interface ReadonlyDiscontinuity {
diff --git a/fbw-a32nx/src/systems/fmgc/src/flightplanning/plans/BaseFlightPlan.ts b/fbw-a32nx/src/systems/fmgc/src/flightplanning/plans/BaseFlightPlan.ts
index 45b97230966..93729688276 100644
--- a/fbw-a32nx/src/systems/fmgc/src/flightplanning/plans/BaseFlightPlan.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/flightplanning/plans/BaseFlightPlan.ts
@@ -33,7 +33,7 @@ import { MissedApproachSegment } from '@fmgc/flightplanning/segments/MissedAppro
import { ArrivalRunwayTransitionSegment } from '@fmgc/flightplanning/segments/ArrivalRunwayTransitionSegment';
import { ApproachViaSegment } from '@fmgc/flightplanning/segments/ApproachViaSegment';
import { SegmentClass } from '@fmgc/flightplanning/segments/SegmentClass';
-import { HoldData, WaypointStats } from '@fmgc/flightplanning/data/flightplan';
+import { HoldData, OffsetData, offsetFlags, WaypointStats } from '@fmgc/flightplanning/data/flightplan';
import { procedureLegIdentAndAnnotation } from '@fmgc/flightplanning/legs/FlightPlanLegNaming';
import {
FlightPlanEvents,
@@ -1451,6 +1451,22 @@ export abstract class BaseFlightPlan
implements
return this.callFunctionViaRpc('enableAltn', atIndexInAlternate, cruiseLevel, planIndex);
}
+ setOffsetParams(
+ startIndex: number,
+ endIndex: number,
+ desiredOffset: OffsetData,
+ planIndex: FlightPlanIndex,
+ alternate?: boolean,
+ ): Promise {
+ return this.callFunctionViaRpc('setOffsetParams', startIndex, endIndex, desiredOffset, planIndex, alternate);
+ }
+
setPilotEnteredAltitudeConstraintAt(
atIndex: number,
isDescentConstraint: boolean,
diff --git a/fbw-a380x/src/systems/instruments/src/MFD/MfdPageDirectory.tsx b/fbw-a380x/src/systems/instruments/src/MFD/MfdPageDirectory.tsx
index 19d1ead9442..201f02a56fa 100644
--- a/fbw-a380x/src/systems/instruments/src/MFD/MfdPageDirectory.tsx
+++ b/fbw-a380x/src/systems/instruments/src/MFD/MfdPageDirectory.tsx
@@ -33,6 +33,7 @@ import { MfdSurvControls } from 'instruments/src/MFD/pages/SURV/MfdSurvControls'
import { MfdFmsFplnFixInfo } from './pages/FMS/F-PLN/MfdFmsFplnFixInfo';
import { MfdSurvStatusSwitching } from 'instruments/src/MFD/pages/SURV/MfdSurvStatusSwitching';
import { MfdFmsDataAirport } from 'instruments/src/MFD/pages/FMS/DATA/MfdFmsDataAirport';
+import { MfdFmsFplnOffset } from 'instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnOffset';
export function pageForUrl(
url: string,
@@ -88,6 +89,11 @@ export function pageForUrl(
case 'fms/sec2/f-pln-hold':
case 'fms/sec3/f-pln-hold':
return ;
+ case 'fms/active/f-pln-offset':
+ case 'fms/sec1/f-pln-offset':
+ case 'fms/sec2/f-pln-offset':
+ case 'fms/sec3/f-pln-offset':
+ return ;
case 'fms/active/f-pln-fix-info':
return ;
case 'fms/position/irs':
diff --git a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/FplnRevisionsMenu.tsx b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/FplnRevisionsMenu.tsx
index b97b7316807..0726e75f291 100644
--- a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/FplnRevisionsMenu.tsx
+++ b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/FplnRevisionsMenu.tsx
@@ -91,8 +91,8 @@ export function getRevisionsMenu(fpln: MfdFmsFpln, type: FplnRevisionsMenuType):
fpln.props.mfd.uiService.navigateTo(`fms/${fpln.props.mfd.uiService.activeUri.get().category}/f-pln-arrival`),
},
{
- name: '(N/A) OFFSET',
- disabled: true,
+ name: 'OFFSET',
+ disabled: !fpln.loadedFlightPlan?.legElementAt(legIndex).isXF,
onPressed: () =>
fpln.props.mfd.uiService.navigateTo(`fms/${fpln.props.mfd.uiService.activeUri.get().category}/f-pln-offset`),
},
diff --git a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnOffset.scss b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnOffset.scss
new file mode 100644
index 00000000000..1e165695632
--- /dev/null
+++ b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnOffset.scss
@@ -0,0 +1,27 @@
+@import "../../../../MsfsAvionicsCommon/definitions";
+
+.mfd-fms-fpln-offset-waypoint-text-grid {
+ padding-top: 20px;
+ display: grid;
+ grid-template-columns: 200px 200px 30px 200px;
+ padding-left: 5px;
+ margin-top: 10px;
+ padding-bottom: 15px;
+}
+
+.mfd-offset-dist-angle-input-grid {
+ display: grid;
+ grid-template-columns: auto;
+}
+
+.mfd-offset-dist-input-grid {
+ display: grid;
+ grid-template-columns: 150px 150px;
+}
+
+.mfd-offset-ret-canc-tmpy-grid {
+ padding-top: 440px;
+ grid-column: span 4;
+ display: grid;
+ grid-template-columns: 251px 256px 248px;
+}
diff --git a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnOffset.tsx b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnOffset.tsx
new file mode 100644
index 00000000000..3beff44f2cd
--- /dev/null
+++ b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnOffset.tsx
@@ -0,0 +1,263 @@
+import { ArraySubject, FSComponent, Subject, VNode } from '@microsoft/msfs-sdk';
+
+import './MfdFmsFpln.scss';
+import './MfdFmsFplnOffset.scss';
+import { AbstractMfdPageProps } from 'instruments/src/MFD/MFD';
+import { FmsPage } from 'instruments/src/MFD/pages/common/FmsPage';
+import { DropdownMenu } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/DropdownMenu';
+import { FlightPlanLeg } from '@fmgc/flightplanning/legs/FlightPlanLeg';
+import { FlightPlanIndex } from '@fmgc/index';
+import { InputField } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/InputField';
+import { OffsetAngleFormat, OffsetDistFormat } from 'instruments/src/MFD/pages/common/DataEntryFormats';
+import { RadioButtonGroup } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/RadioButtonGroup';
+import { Footer } from 'instruments/src/MFD/pages/common/Footer';
+import { Button } from 'instruments/src/MsfsAvionicsCommon/UiWidgets/Button';
+
+interface MfdFmsFplnOffsetProps extends AbstractMfdPageProps {}
+
+export class MfdFmsFplnOffset extends FmsPage {
+ private dropdownMenuRef = FSComponent.createRef();
+
+ private returnButtonDiv = FSComponent.createRef();
+
+ private cancelButtonDiv = FSComponent.createRef();
+
+ private tmpyInsertButtonDiv = FSComponent.createRef();
+
+ private availableWaypoints = ArraySubject.create([]);
+
+ private availableWaypointsToLegIndex: number[] = [];
+
+ private selectedStartWaypointIndex = Subject.create(null);
+
+ private selectedEndWaypointIndex = Subject.create(null);
+
+ private manualWptIdent: string | null = '';
+
+ private utcEta = Subject.create('--:--');
+
+ private distToWpt = Subject.create('---');
+
+ private offsetInterceptAngle = Subject.create(null);
+
+ private offsetDist = Subject.create(null);
+
+ private OffsetLRIndex = Subject.create(1);
+
+ protected onNewData(): void {
+ this.offsetInterceptAngle.set(30);
+ this.offsetDist.set(5);
+ this.OffsetLRIndex.set(0);
+ const activeLegIndex = this.props.fmcService.master?.flightPlanService.get(
+ this.loadedFlightPlanIndex.get(),
+ ).activeLegIndex;
+ if (activeLegIndex) {
+ const wpt = this.loadedFlightPlan?.allLegs
+ .slice(activeLegIndex + 1)
+ .map((el) => {
+ if (el.isDiscontinuity === false) {
+ return el.ident;
+ }
+ return null;
+ })
+ .filter((el) => el !== null) as string[] | undefined;
+ if (wpt) {
+ this.availableWaypoints.set(wpt);
+ }
+
+ const revWptIdx = this.props.fmcService.master?.revisedWaypointIndex.get();
+ if (revWptIdx && this.props.fmcService.master?.revisedWaypointIndex.get() !== undefined) {
+ this.selectedStartWaypointIndex.set(revWptIdx - activeLegIndex - 1);
+ }
+ }
+
+ // Use active FPLN for building the list (page only works for active anyways)
+ const activeFpln = this.props.fmcService.master?.flightPlanService.active;
+ if (activeFpln) {
+ this.availableWaypointsToLegIndex = [];
+ const wpt = activeFpln.allLegs
+ .slice(activeFpln.activeLegIndex, activeFpln.firstMissedApproachLegIndex)
+ .map((el, idx) => {
+ if (el instanceof FlightPlanLeg && el.isXF()) {
+ this.availableWaypointsToLegIndex.push(idx + activeFpln.activeLegIndex);
+ return el.ident;
+ }
+ return null;
+ })
+ .filter((el) => el !== null) as readonly string[];
+ if (wpt) {
+ this.availableWaypoints.set(wpt);
+ }
+ }
+
+ // Existance of TMPY fpln tells us that an offset is pending
+ if (this.loadedFlightPlanIndex.get() === FlightPlanIndex.Temporary) {
+ // If waypoint was revised select revised wpt
+ const revWpt = this.props.fmcService.master?.revisedWaypoint();
+ if (revWpt) {
+ const selectedLegIndex = this.availableWaypoints.getArray().findIndex((it) => it === revWpt.ident);
+ if (selectedLegIndex !== -1) {
+ this.selectedStartWaypointIndex.set(selectedLegIndex);
+ }
+ }
+
+ // Manual waypoint was entered. In this case, force dropdown field to display wpt ident without selecting it
+ if (this.manualWptIdent) {
+ this.selectedStartWaypointIndex.set(null);
+ this.dropdownMenuRef.instance.forceLabel(this.manualWptIdent);
+ }
+
+ //TODO Display ETA; target waypoint is now activeLeg termination in temporary fpln
+ if (this.loadedFlightPlan?.activeLeg instanceof FlightPlanLeg) {
+ // No predictions for temporary fpln atm, so only distance is displayed
+ this.distToWpt.set(this.loadedFlightPlan?.activeLeg?.calculated?.cumulativeDistance?.toFixed(0) ?? '---');
+ }
+ }
+ }
+
+ public onAfterRender(node: VNode): void {
+ super.onAfterRender(node);
+
+ this.subs.push(
+ this.tmpyActive.sub((v) => {
+ if (this.returnButtonDiv.getOrDefault() && this.tmpyInsertButtonDiv.getOrDefault()) {
+ this.returnButtonDiv.instance.style.visibility = v ? 'hidden' : 'visible';
+ this.tmpyInsertButtonDiv.instance.style.visibility = v ? 'hidden' : 'visible';
+ }
+ }, true),
+ );
+ }
+
+ render(): VNode {
+ return (
+ <>
+ {super.render()}
+ {/* begin page content */}
+