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 */} +

+
+
+
+
+ START WPT +
+
+ +
+
+
+
+ END WPT +
+
+ +
+
+
+
+
+ INTERCEPT ANGLE +
+
+ + dataEntryFormat={new OffsetAngleFormat()} + value={this.offsetInterceptAngle} + errorHandler={(e) => this.props.fmcService.master?.showFmsErrorMessage(e)} + hEventConsumer={this.props.mfd.hEventConsumer} + interactionMode={this.props.mfd.interactionMode} + /> +
+
+ OFFSET DIST +
+
+
+ + dataEntryFormat={new OffsetDistFormat()} + value={this.offsetDist} + errorHandler={(e) => this.props.fmcService.master?.showFmsErrorMessage(e)} + hEventConsumer={this.props.mfd.hEventConsumer} + interactionMode={this.props.mfd.interactionMode} + /> +
+
+ +
+
+
+
+
+
+
+
, + )} + onClick={() => { + this.props.mfd.uiService.navigateTo('back'); + }} + /> +
+
+
+
+
+
+
+ {/* end page content */} +