From 8a8240fdd473a677712a4886c6b2b6eef6e8997b Mon Sep 17 00:00:00 2001 From: Matt Nischan Date: Thu, 7 Dec 2023 16:45:18 -0600 Subject: [PATCH] SU14 SDK update. --- src/garminsdk/autopilot/GarminAPConfig.ts | 38 +- .../autopilot/GarminAPConfigInterface.ts | 12 + .../autopilot/GarminAPStateManager.ts | 9 + src/garminsdk/autopilot/GarminAutopilot.ts | 16 +- .../autopilot/GarminNavToNavManager.ts | 3 +- src/garminsdk/autopilot/GarminVNavManager.ts | 3 +- src/garminsdk/autopilot/GarminVNavManager2.ts | 3 +- src/garminsdk/autopilot/index.ts | 1 + src/garminsdk/components/cas/CAS.tsx | 30 +- .../components/common/BearingDisplay.tsx | 143 +- .../components/common/LatLonDisplay.tsx | 32 +- .../components/common/NumberUnitDisplay.tsx | 133 +- .../components/common/TimeDisplay.tsx | 67 +- src/garminsdk/components/list/ScrollList.tsx | 91 +- .../components/map/GarminMapBuilder.tsx | 189 +- src/garminsdk/components/map/GarminMapKeys.ts | 14 + .../components/map/MapRangeDisplay.tsx | 3 +- .../components/map/MapResourcePriority.ts | 6 + .../components/map/MapWaypointIcon.ts | 16 +- .../map/NextGenGarminMapBuilder.tsx | 97 + .../assembled/NextGenConnextMapBuilder.tsx | 22 +- .../map/assembled/NextGenHsiMapBuilder.tsx | 3 +- .../map/assembled/NextGenNavMapBuilder.tsx | 26 +- .../assembled/NextGenNearestMapBuilder.tsx | 22 +- .../assembled/NextGenWaypointMapBuilder.tsx | 6 +- .../MapDataIntegrityRTRController.ts | 70 +- .../MapDesiredOrientationController.ts | 97 + .../MapFlightPlanFocusRTRController.ts | 20 +- .../controllers/MapOrientationController.ts | 10 +- .../MapOrientationModeController.ts | 69 + .../MapOrientationRTRController.ts | 11 +- .../MapOrientationSettingsController.ts | 93 + .../controllers/MapPanningRTRController.ts | 206 ++ .../controllers/MapPointerRTRController.ts | 199 +- .../map/controllers/MapTerrainController.ts | 54 +- .../controllers/MapWaypointsVisController.ts | 73 +- .../WeatherMapOrientationController.ts | 14 +- ...WeatherMapOrientationSettingsController.ts | 138 ++ .../components/map/controllers/index.ts | 7 +- src/garminsdk/components/map/index.ts | 3 +- .../MapRelativeTerrainStatusIndicator.tsx | 6 +- .../map/layers/MapRangeCompassLayer.tsx | 62 +- .../map/layers/MapWaypointsLayer.tsx | 2 +- .../map/layers/TrafficMapRangeLayer.tsx | 207 +- .../map/modules/MapOrientationModule.ts | 20 +- .../map/modules/MapPanningModule.ts | 12 + .../map/modules/MapPointerModule.ts | 4 +- .../map/modules/MapTrackVectorModule.ts | 2 +- .../map/modules/MapWaypointsModule.ts | 5 +- src/garminsdk/components/map/modules/index.ts | 1 + .../DefaultNavDataBarFieldModelFactory.ts | 17 +- ...EventBusNavDataBarFieldTypeModelFactory.ts | 23 + .../GenericNavDataBarFieldModelFactory.ts | 578 +----- .../components/navdatabar/NavDataBar.tsx | 2 +- .../navdatabar/NavDataBarFieldModel.ts | 12 + .../NavDataBarFieldTypeModelFactories.ts | 851 ++++++++ .../NextGenNavDataBarFieldRenderer.ts | 4 +- src/garminsdk/components/navdatabar/index.ts | 2 + .../components/navdatafield/NavDataField.tsx | 60 +- .../navdatafield/NavDataFieldModel.ts | 5 + .../navdatafield/NavDataFieldType.ts | 143 +- .../NavDataFieldTypeRenderers.tsx | 240 +++ ...x => NextGenNavDataFieldTypeRenderers.tsx} | 352 ++-- .../components/navdatafield/index.ts | 3 +- .../airspeed/AirspeedDefinitionFactory.ts | 3 + .../nextgenpfd/airspeed/AirspeedIndicator.tsx | 104 +- .../airspeed/AirspeedIndicatorDataProvider.ts | 40 +- .../nextgenpfd/horizon/ArtificialHorizon.tsx | 229 ++- .../nextgenpfd/horizon/HorizonDisplay.tsx | 8 +- .../nextgenpfd/horizon/PitchLadder.tsx | 5 +- .../components/nextgenpfd/horizon/index.ts | 1 + .../navstatusbox/NavStatusBoxFieldModel.ts | 2 +- .../NavStatusBoxFieldRenderer.tsx | 3 +- .../components/touchbutton/TouchButton.tsx | 3 + .../components/touchpad/TouchPad.tsx | 2 + .../components/waypoint/WaypointIcon.tsx | 51 +- src/garminsdk/flightplan/Fms.ts | 64 +- src/garminsdk/flightplan/FmsUtils.ts | 3 +- src/garminsdk/graphics/text/UnitFormatter.ts | 23 +- .../navigation/NavIndicatorController.ts | 5 +- src/garminsdk/navigation/NavdataComputer.ts | 3 +- src/garminsdk/navigation/VNavDataProvider.ts | 3 +- src/garminsdk/navigation/WaypointInfoStore.ts | 47 +- .../settings/DateTimeUserSettings.ts | 73 +- src/garminsdk/settings/MapUserSettings.ts | 11 +- src/garminsdk/settings/UnitsUserSettings.ts | 380 ++-- src/garminsdk/settings/VSpeedUserSettings.ts | 21 +- .../settings/WeatherMapUserSettings.ts | 1 + src/garminsdk/system/AdcSystem.ts | 21 +- src/garminsdk/system/GpsReceiverSystem.ts | 36 +- .../traffic/AdsbSensitivityParameters.ts | 2 +- src/garminsdk/traffic/GarminTcasII.ts | 72 +- .../traffic/TrafficAdvisorySystem.ts | 77 +- src/garminsdk/traffic/TrafficInfoService.ts | 296 ++- src/msfstypes/coherent/apcontroller.d.ts | 33 + src/msfstypes/coherent/facilities.d.ts | 3 + src/msfstypes/index.d.ts | 1 + src/msfstypes/js/animation/animation.d.ts | 173 ++ src/msfstypes/js/avionics.d.ts | 150 ++ src/msfstypes/js/buttons.d.ts | 106 + src/msfstypes/js/common.d.ts | 1133 +++++++++++ src/msfstypes/js/datastorage.d.ts | 7 + src/msfstypes/js/datavalidator.d.ts | 15 + src/msfstypes/js/flight.d.ts | 298 +++ src/msfstypes/js/inputs.d.ts | 21 + src/msfstypes/js/leaderboards.d.ts | 65 + src/msfstypes/js/netbingmap.d.ts | 102 + src/msfstypes/js/padscroll.d.ts | 110 ++ src/msfstypes/js/radionav.d.ts | 6 + src/msfstypes/js/services/aircraft.d.ts | 61 + .../js/services/checklistmanager.d.ts | 123 ++ src/msfstypes/js/services/community.d.ts | 251 +++ src/msfstypes/js/services/contentmanager.d.ts | 60 + src/msfstypes/js/services/controls.d.ts | 182 ++ src/msfstypes/js/services/fuelpayload.d.ts | 56 + src/msfstypes/js/services/gamenavlog.d.ts | 54 + .../js/services/genericpanelmanager.d.ts | 8 + .../ingamepanelcontrolsviewlistener.d.ts | 5 + .../js/services/installationmanager.d.ts | 22 + src/msfstypes/js/services/market.d.ts | 282 +++ src/msfstypes/js/services/notifications.d.ts | 59 + src/msfstypes/js/services/objective.d.ts | 19 + src/msfstypes/js/services/profile.d.ts | 111 ++ .../js/services/raceleaderboard.d.ts | 9 + src/msfstypes/js/services/replay.d.ts | 114 ++ .../js/services/rewardscreenmanager.d.ts | 107 + src/msfstypes/js/services/toolbarpanels.d.ts | 76 + .../js/services/tooltipsmanager.d.ts | 23 + src/msfstypes/js/services/weather.d.ts | 133 ++ src/msfstypes/js/simplane.d.ts | 588 ++++++ src/msfstypes/js/simvar.d.ts | 46 + src/msfstypes/js/slider.d.ts | 51 + src/msfstypes/js/sortedlist.d.ts | 11 + src/msfstypes/js/testsimvar.d.ts | 10 + src/msfstypes/js/types.d.ts | 108 ++ src/msfstypes/js/uiresourceelement.d.ts | 136 ++ src/msfstypes/js/wasmsimcanvas.d.ts | 24 + src/msfstypes/js/widgetscontainer.d.ts | 52 + src/msfstypes/package.json | 21 + .../pages/vcockpit/core/vcockpit.d.ts | 42 + .../pages/vcockpit/core/vcockpitlogic.d.ts | 39 + .../instruments/shared/baseinstrument.d.ts | 149 ++ .../shared/flightelements/approach.d.ts | 19 + .../shared/flightelements/flightplan.d.ts | 36 + .../flightelements/flightplanmanager.d.ts | 187 ++ .../shared/flightelements/geocalc.d.ts | 18 + .../flightelements/nearestwaypoint.d.ts | 192 ++ .../shared/flightelements/runway.d.ts | 21 + .../shared/flightelements/waypoint.d.ts | 205 ++ .../shared/flightelements/waypointloader.d.ts | 242 +++ .../shared/utils/datareadmanager.d.ts | 24 + .../instruments/shared/utils/radionav.d.ts | 103 + .../instruments/shared/utils/xmllogic.d.ts | 209 ++ .../pages/vcockpit/systems/systems.d.ts | 16 + .../vcockpit/systems/systems_as1000.d.ts | 22 + src/sdk/autopilot/APConfig.ts | 22 +- src/sdk/autopilot/Autopilot.ts | 55 +- src/sdk/autopilot/directors/APFLCDirector.ts | 35 +- .../autopilot/directors/APPitchLvlDirector.ts | 66 + src/sdk/autopilot/directors/APVSDirector.ts | 4 +- src/sdk/autopilot/directors/index.ts | 1 + src/sdk/autopilot/index.ts | 1 + src/sdk/autopilot/managers/APStateManager.ts | 19 +- .../managers/AltitudeSelectManager.ts | 94 +- src/sdk/autothrottle/AbstractAutothrottle.ts | 29 +- src/sdk/cas/CasSystem.ts | 19 +- src/sdk/components/FSComponent.ts | 5 +- .../XMLGauges/GaugeDefinitions/BaseGauge.ts | 2 +- .../components/XMLGauges/XMLGaugeAdapter.ts | 62 +- src/sdk/components/bing/BingComponent.tsx | 6 +- src/sdk/components/common/DigitScroller.tsx | 4 +- .../components/controls/HardwareUiControl.tsx | 4 +- .../controls/HardwareUiControlList.tsx | 5 + .../map/DefaultMapLabeledRingLabel.tsx | 138 ++ src/sdk/components/map/MapLabeledRingLabel.ts | 57 + .../map/MapMultiLineAirspaceRenderer.ts | 2 +- src/sdk/components/map/index.ts | 61 +- .../layers/GenericMapSharedCanvasSubLayer.ts | 77 + .../layers/MapLabeledRingCanvasSubLayer.tsx | 238 +++ .../map/layers/MapLabeledRingLayer.tsx | 159 +- .../map/layers/MapSharedCanvasLayer.tsx | 357 ++++ .../components/mapsystem/MapSystemBuilder.tsx | 48 +- .../components/mapsystem/MapSystemUtils.ts | 53 +- .../controllers/MapRotationController.ts | 2 +- .../mapsystem/modules/MapRotationModule.ts | 5 +- src/sdk/data/Consumer.ts | 2 +- src/sdk/data/ConsumerSubject.ts | 26 +- src/sdk/data/ConsumerValue.ts | 24 +- src/sdk/data/SimVars.ts | 14 + src/sdk/fmc/AbstractFmcPage.ts | 10 +- src/sdk/fmc/FmcFormat.ts | 22 +- src/sdk/fmc/FmcScreen.ts | 15 +- src/sdk/fmc/SimpleFmcRenderer.ts | 2 +- src/sdk/fmc/components/DisplayField.ts | 22 +- src/sdk/fmc/components/TextInputField.ts | 31 +- src/sdk/graphics/color/ColorUtils.ts | 149 +- src/sdk/graphics/text/NumberFormatter.ts | 91 +- src/sdk/instruments/ADC.ts | 515 ++++- src/sdk/instruments/Accelerometer.ts | 35 + .../instruments/AircraftInertialPublisher.ts | 55 + src/sdk/instruments/BasePublishers.ts | 24 +- src/sdk/instruments/Clock.ts | 14 +- src/sdk/instruments/ControlSurfaces.ts | 4 + src/sdk/instruments/EngineData.ts | 21 +- src/sdk/instruments/GNSS.ts | 4 + src/sdk/instruments/GPSSat.ts | 1714 ++++++++++++++--- src/sdk/instruments/RadioCommon.ts | 3 +- src/sdk/instruments/index.ts | 2 + src/sdk/math/NumberUnit.ts | 14 +- src/sdk/navigation/Facilities.ts | 2 +- src/sdk/simbrief/index.ts | 3 +- src/sdk/sub/AbstractSubscribable.ts | 2 +- src/sdk/sub/CombinedSubject.ts | 3 + src/sdk/sub/ComputedSubject.ts | 2 +- src/sdk/sub/MappedSubject.ts | 3 +- src/sdk/sub/Subscribable.ts | 5 +- src/sdk/sub/SubscribableArray.ts | 2 +- src/sdk/sub/SubscribableMapFunctions.ts | 19 + src/sdk/traffic/TCAS.ts | 49 +- src/sdk/utils/datastructures/ArrayUtils.ts | 33 +- .../Assets/Fonts/DejaVuSans-SemiBold.ttf | Bin 0 -> 732148 bytes .../EngineInstruments/DialGauge.tsx | 28 +- .../EngineInstruments/HorizontalBarGauge.tsx | 62 +- .../EngineInstruments/VerticalBarGauge.tsx | 28 +- .../Components/UI/FPL/ConstraintSelector.tsx | 70 +- .../Components/UI/FPL/MFDFPLVNavProfile.tsx | 22 +- .../MFD/Components/UI/MFDPageMenuDialog.css | 43 +- .../MFD/Components/UI/MFDPageMenuDialog.tsx | 4 +- .../MFD/Components/UI/MFDPageSelect.tsx | 9 +- .../WTG1000/MFD/Components/UI/MFDUiPage.tsx | 11 + .../MFD/Components/UI/MFDViewService.ts | 26 +- .../UI/NavDataBar/MFDNavDataBar.css | 52 +- .../UI/Nearest/Airports/FrequenciesGroup.tsx | 6 +- .../UI/Nearest/MFDNearestAirportsPage.tsx | 115 +- .../MFD/Components/UI/Procedure/MFDProc.tsx | 1 + .../UI/Procedure/ProcSequenceItem.tsx | 2 +- .../MFDSystemSetupDataBarGroup.tsx | 24 +- .../MFDSystemSetupDateTimeGroup.tsx | 10 +- .../UI/SystemSetup/MFDSystemSetupRow.tsx | 2 +- .../NavSystems/WTG1000/MFD/WTG1000_MFD.tsx | 96 +- .../FlightInstruments/AirspeedIndicator.css | 260 --- .../FlightInstruments/AirspeedIndicator.tsx | 883 --------- .../G1000AirspeedIndicator.css | 679 +++++++ .../G1000AirspeedIndicator.tsx | 77 + .../G1000AirspeedIndicatorDataProvider.ts | 19 + .../AirspeedIndicator/index.ts | 2 + .../FlightInstruments/Altimeter.tsx | 33 +- .../PFD/Components/FlightInstruments/CAS.css | 13 +- .../PFD/Components/FlightInstruments/CAS.tsx | 406 ++-- .../PFD/Components/FlightInstruments/index.ts | 1 + .../Components/Overlays/BottomInfoPanel.css | 17 +- .../PFD/Components/Overlays/Fma/Fma.tsx | 14 + .../PFD/Components/Overlays/Transponder.css | 12 +- .../PFD/Components/Overlays/Transponder.tsx | 103 +- .../PFD/Components/UI/Alerts/AlertsSubject.ts | 48 +- .../Components/UI/Alerts/CasAlertsBridge.ts | 74 + .../WTG1000/PFD/Components/UI/Alerts/index.ts | 1 + .../PFD/Components/UI/FPL/FPLSection.tsx | 63 +- .../Components/UI/FPL/FPLSectionDeparture.tsx | 74 +- .../UI/FPL/FPLSectionDestination.tsx | 39 +- .../PFD/Components/UI/PFDViewService.ts | 21 +- .../PFD/Components/UI/TimerRef/TimerRef.css | 96 +- .../PFD/Components/UI/TimerRef/TimerRef.tsx | 423 ++-- .../NavSystems/WTG1000/PFD/WTG1000_PFD.tsx | 161 +- .../Shared/Autopilot/G1000Autopilot.ts | 71 +- .../WTG1000/Shared/Config/Config.ts | 34 + .../Shared/Config/DefaultConfigFactory.ts | 28 + .../Shared/Config/LookupTableConfig.ts | 73 + .../WTG1000/Shared/Config/NumericConfig.ts | 321 +++ .../WTG1000/Shared/Config/SpeedConfig.ts | 209 ++ .../NavSystems/WTG1000/Shared/Config/index.ts | 5 + .../NavSystems/WTG1000/Shared/FuelComputer.ts | 45 +- .../NavSystems/WTG1000/Shared/G1000Events.ts | 14 +- .../WTG1000/Shared/G1000PfdPlugin.ts | 16 + .../NavSystems/WTG1000/Shared/G1000Plugin.ts | 51 +- .../Input/AltimeterBaroKeyEventHandler.ts | 11 +- .../Shared/Input/ControlpadHEventHandler.ts | 121 ++ .../MapRelativeTerrainStatusIndicator.css | 2 + .../WTG1000/Shared/Map/MapUserSettings.ts | 4 + .../Shared/NavCom/NavComFrequencyElement.tsx | 321 ++- .../WTG1000/Shared/NavCom/NavComRadio.css | 52 +- .../WTG1000/Shared/NavCom/NavComRadio.tsx | 96 +- .../AirspeedIndicatorConfig.ts | 389 ++++ .../AirspeedIndicator/ColorRangeConfig.ts | 103 + .../AirspeedIndicator/VSpeedBugConfig.ts | 73 + .../Profiles/AirspeedIndicator/index.ts | 3 + .../Profiles/Autopilot/AutopilotConfig.ts | 266 +++ .../Shared/Profiles/Autopilot/index.ts | 1 + .../Profiles/G1000AirframeOptionsManager.ts | 153 ++ .../Profiles/G1000SettingSaveManager.ts | 2 +- .../WTG1000/Shared/Profiles/index.ts | 3 + .../NavSystems/WTG1000/Shared/StartupLogo.tsx | 6 +- .../WTG1000/Shared/UI/Common/g1k_common.css | 23 +- .../Controllers/ControlpadInputController.ts | 434 +++++ .../UI/Controllers/XpdrInputController.ts | 81 + .../WTG1000/Shared/UI/DirectTo/DirectTo.tsx | 6 +- .../NavSystems/WTG1000/Shared/UI/FmsHEvent.ts | 49 +- .../WTG1000/Shared/UI/G1000UiControl.tsx | 1007 +++++++++- .../WTG1000/Shared/UI/Menus/MFD/LeanMenu.ts | 1 - .../WTG1000/Shared/UI/Menus/RootMenu.ts | 49 +- .../WTG1000/Shared/UI/Menus/SoftKey.css | 82 +- .../WTG1000/Shared/UI/Menus/SoftKey.tsx | 28 +- .../WTG1000/Shared/UI/Menus/SoftKeyMenu.ts | 11 +- .../WTG1000/Shared/UI/Menus/XPDRCodeMenu.ts | 12 +- .../WTG1000/Shared/UI/Menus/XPDRMenu.ts | 5 +- .../Shared/UI/UIControls/ArrowToggle.tsx | 7 +- .../Shared/UI/UIControls/InputComponent.tsx | 97 +- .../Shared/UI/UIControls/NumberInput.tsx | 4 +- .../Shared/UI/UIControls/SelectControl.tsx | 2 +- .../Shared/UI/UIControls/WaypointInput.tsx | 2 +- .../Shared/UI/UiControls2/ArrowControl.css | 27 + .../Shared/UI/UiControls2/ArrowControl.tsx | 201 ++ .../UI/UiControls2/CourseNumberInput.tsx | 58 + .../UI/UiControls2/G1000UiControlWrapper.tsx | 4 +- .../UI/UiControls2/GenericNumberInput.tsx | 98 +- .../Shared/UI/UiControls2/TimeNumberInput.tsx | 59 + .../WTG1000/Shared/UI/UiControls2/index.ts | 3 + .../UserSettings/UserSettingSelectControl.tsx | 2 +- .../WTG1000/Shared/UI/ViewService.ts | 18 +- .../WTG1000/Shared/UI/WptInfo/WptInfo.tsx | 1 + .../WTG1000/Shared/Units/UnitsUserSettings.ts | 2 +- .../WTG1000/Shared/VSpeed/VSpeed.ts | 44 + .../WTG1000/Shared/VSpeed/VSpeedConfig.ts | 61 + .../Shared/VSpeed/VSpeedGroupConfig.ts | 47 + .../Shared/VSpeed/VSpeedUserSettings.ts | 87 + .../NavSystems/WTG1000/Shared/VSpeed/index.ts | 3 + .../NavSystems/WTG1000/Shared/index.ts | 4 + .../NavSystems/WTG1000/package.json | 18 + .../package.json | 4 +- .../rollup-defs.config.js | 12 - .../Assets/Fonts/DejaVuSans-SemiBold.ttf | Bin 731016 -> 732148 bytes .../GTC/Components/CharInput/CharInput.tsx | 319 +++ .../Components/CharInput/CharInputSlot.tsx | 267 +++ .../WTG3000/GTC/Components/CharInput/index.ts | 2 + .../Components/CursorInput/CursorInput.css | 1 + .../Components/CursorInput/CursorInput.tsx | 28 +- .../CursorInput/CursorInputSlot.css | 1 + .../CursorInput/CursorInputSlot.tsx | 18 +- .../GTC/Components/Keyboard/Keyboard.css | 191 ++ .../GTC/Components/Keyboard/Keyboard.tsx | 239 +++ .../WTG3000/GTC/Components/Keyboard/index.ts | 1 + .../Components/LatLonInput/LatLonInput.tsx | 19 +- .../TouchButton/GtcListSelectTouchButton.tsx | 6 +- .../TouchButton/GtcWaypointSelectButton.tsx | 19 +- .../Components/TouchSlider/TouchSlider.css | 14 +- .../Waypoint/GtcWaypointDisplay.tsx | 45 +- .../WTG3000/GTC/Components/index.ts | 2 + .../GTC/Dialog/AbstractGtcNumberDialog.css | 4 +- .../WTG3000/GTC/Dialog/GtcDistanceDialog.tsx | 2 +- .../WTG3000/GTC/Dialog/GtcFrequencyDialog.css | 4 +- .../WTG3000/GTC/Dialog/GtcKeyboardDialog.tsx | 39 +- .../WTG3000/GTC/Dialog/GtcLatLonDialog.css | 4 +- .../WTG3000/GTC/Dialog/GtcLatLonDialog.tsx | 8 +- .../WTG3000/GTC/Dialog/GtcMessageDialog.tsx | 2 +- .../GTC/Dialog/GtcUserWaypointDialog.tsx | 15 +- .../WTG3000/GTC/Dialog/GtcWaypointDialog.css | 126 ++ .../WTG3000/GTC/Dialog/GtcWaypointDialog.tsx | 549 ++++++ .../NavSystems/WTG3000/GTC/Dialog/index.ts | 1 + .../WTG3000/GTC/GtcService/GtcViewKeys.ts | 4 +- .../GtcAvionicsSettingsPageMfdFieldsList.tsx | 6 +- .../FlightPlanPage/GtcFlightPlanDialogs.tsx | 41 +- .../Pages/NavComHome/GtcAudioRadiosPopup.css | 1 + .../Procedures/GtcProcedureSelectionPage.tsx | 3 +- .../WTG3000/GTC/WTG3000GtcInstrument.tsx | 52 +- .../WTG3000/GTC/package-defs-only.json | 2 +- .../NavSystems/WTG3000/GTC/package.json | 11 +- .../WTG3000/GTC/rollup-defs.config.js | 12 - .../NavSystems/WTG3000/GTC/tsconfig.json | 2 +- .../WTG3000/MFD/WTG3000MfdInstrument.tsx | 19 +- .../WTG3000/MFD/package-defs-only.json | 2 +- .../NavSystems/WTG3000/MFD/package.json | 11 +- .../WTG3000/MFD/rollup-defs.config.js | 12 - .../NavSystems/WTG3000/MFD/tsconfig.json | 2 +- .../Components/Airspeed/AirspeedIndicator.tsx | 1 - .../Components/Airspeed/ColorRangeConfig.ts | 2 +- .../WTG3000/PFD/Components/Fma/Fma.tsx | 2 + .../PFD/Components/Horizon/HorizonDisplay.tsx | 7 +- .../WTG3000/PFD/WTG3000PfdInstrument.tsx | 10 +- .../WTG3000/PFD/package-defs-only.json | 2 +- .../NavSystems/WTG3000/PFD/package.json | 11 +- .../WTG3000/PFD/rollup-defs.config.js | 12 - .../NavSystems/WTG3000/PFD/tsconfig.json | 2 +- .../package.json | 2 +- .../Shared/Autopilot/G3000Autopilot.ts | 6 +- .../MapRelativeTerrainStatusIndicator.css | 2 + .../FlightPlanTextInset/FlightPlanTextRow.tsx | 6 +- .../WTG3000/Shared/Config/NumericConfig.ts | 2 + .../WTG3000/Shared/Config/SpeedConfig.ts | 34 +- .../NavSystems/WTG3000/Shared/G3000Version.ts | 4 +- .../Shared/NavReference/G3000NavReference.ts | 3 +- .../Settings/G3000UserSettingSaveManager.ts | 4 +- .../Shared/Settings/MapUserSettings.ts | 2 + .../WTG3000/Shared/WTG3000FsInstrument.ts | 8 +- .../NavSystems/WTG3000/Shared/package.json | 13 +- .../WTG3000/Shared/package.json.src | 2 +- .../WTG3000/Shared/rollup-defs.config.js | 12 - .../wtg3000common/package.json | 2 +- .../GPS/Shared/Autopilot/GNSVNavManager.ts | 3 +- .../NavSystems/GPS/Shared/MainScreen.tsx | 10 +- .../NavSystems/GPS/Shared/StartupScreen.tsx | 4 +- .../UI/DataFields/GNSDataFieldRenderer.tsx | 4 +- .../UI/Pages/Waypoint/ProcDeparturePage.tsx | 19 +- src/workingtitle-instruments-gns/package.json | 4 +- .../WT21/FMC/Pages/ApproachRefPage.ts | 10 +- .../WT21/FMC/Pages/TakeoffRefPage.ts | 10 +- .../Instruments/WT21/MFD/MFDUserSettings.tsx | 13 + .../WT21/MFD/Menus/MfdLwrMenuViewService.ts | 185 +- .../WT21/MFD/Menus/MfdUprMenuViewService.ts | 10 +- .../WT21/MFD/WT21_MFD_Instrument.tsx | 42 +- .../FlightInstruments/AirspeedIndicator.tsx | 10 +- .../Instruments/WT21/PFD/Menus/PfdMenu.tsx | 6 +- .../WT21/PFD/Menus/PfdMenuViewService.ts | 208 +- .../WT21/PFD/Menus/PfdRefsMenu.tsx | 46 +- .../PFD/Menus/PfdSideButtonsNavBrgSrcMenu.css | 32 + .../PFD/Menus/PfdSideButtonsNavBrgSrcMenu.tsx | 205 ++ .../PFD/Menus/PfdSideButtonsRefs1Menu.tsx | 265 +++ .../PFD/Menus/PfdSideButtonsRefs2Menu.css | 11 + .../PFD/Menus/PfdSideButtonsRefs2Menu.tsx | 137 ++ .../WT21/PFD/Menus/PfdSideButtonsRefsMenu.css | 35 + .../WT21/PFD/WT21_PFD_Instrument.tsx | 68 +- .../WT21/Shared/Config/DisplayUnitConfig.ts | 56 + .../WT21/Shared/FlightPlan/WT21Fms.ts | 442 +++-- .../WT21/Shared/FlightPlan/WT21FmsUtils.ts | 12 + .../WT21/Shared/FlightPlanAsoboSync.ts | 17 +- .../WT21/Shared/LowerSection/HSI/MfdHsi.tsx | 30 +- .../LeftInfoPanel/ElapsedTimeDisplay.css | 28 + .../LeftInfoPanel/ElapsedTimeDisplay.tsx | 30 +- .../LeftInfoPanel/LeftInfoPanel.tsx | 35 +- .../LeftInfoPanel/NavSourcePreset.css | 24 + .../LeftInfoPanel/NavSourcePreset.tsx | 28 +- .../LowerSection/LowerSectionContainer.tsx | 16 +- .../RightInfoPanel/FormatInfo.css | 55 + .../RightInfoPanel/FormatSwitch.tsx | 57 + .../RightInfoPanel/MinimumsDisplay.css | 9 +- .../RightInfoPanel/RightInfoPanel.css | 7 +- .../RightInfoPanel/RightInfoPanel.tsx | 56 +- .../RightInfoPanel/TerrWxInfo.css | 48 +- .../RightInfoPanel/TerrWxInfo.tsx | 84 +- .../LowerSection/RightInfoPanel/TfcInfo.css | 40 +- .../LowerSection/RightInfoPanel/TfcInfo.tsx | 37 +- .../WT21/Shared/Menus/Components/Checkbox.css | 58 +- .../Menus/Components/CheckboxNumeric.tsx | 92 +- .../Menus/Components/CyclicRadioItem.css | 21 + .../Menus/Components/CyclicRadioList.css | 125 ++ .../Menus/Components/FloatingRadioItem.tsx | 55 + .../Menus/Components/FloatingRadioList.tsx | 145 ++ .../Shared/Menus/Components/PopupSubMenu.tsx | 22 +- .../Shared/Menus/Components/RadioList.css | 49 +- .../Shared/Menus/Components/RadioList.tsx | 75 +- .../WT21/Shared/Menus/Components/Radiobox.tsx | 29 +- .../WT21/Shared/Menus/MenuContainer.css | 8 + .../Shared/Navigation/WT21NavIndicators.ts | 29 +- .../Shared/Profiles/VSpeedUserSettings.ts | 22 +- .../Shared/Profiles/WT21SettingSaveManager.ts | 14 +- .../Instruments/WT21/Shared/UI/GuiHEvent.ts | 8 + .../WT21/Shared/UI/MenuViewService.ts | 13 +- .../WT21/Shared/UI/WT21UiControl.tsx | 50 +- .../Shared/WT21DisplayUnitFsInstrument.ts | 113 ++ .../Instruments/WT21/Shared/WT21_Common.css | 1 + 459 files changed, 26814 insertions(+), 5442 deletions(-) create mode 100644 src/garminsdk/autopilot/GarminAPConfigInterface.ts create mode 100644 src/garminsdk/components/map/NextGenGarminMapBuilder.tsx create mode 100644 src/garminsdk/components/map/controllers/MapDesiredOrientationController.ts create mode 100644 src/garminsdk/components/map/controllers/MapOrientationModeController.ts create mode 100644 src/garminsdk/components/map/controllers/MapOrientationSettingsController.ts create mode 100644 src/garminsdk/components/map/controllers/MapPanningRTRController.ts create mode 100644 src/garminsdk/components/map/controllers/WeatherMapOrientationSettingsController.ts create mode 100644 src/garminsdk/components/map/modules/MapPanningModule.ts create mode 100644 src/garminsdk/components/navdatabar/EventBusNavDataBarFieldTypeModelFactory.ts create mode 100644 src/garminsdk/components/navdatabar/NavDataBarFieldTypeModelFactories.ts create mode 100644 src/garminsdk/components/navdatafield/NavDataFieldTypeRenderers.tsx rename src/garminsdk/components/navdatafield/{NextGenNavDataFieldRenderers.tsx => NextGenNavDataFieldTypeRenderers.tsx} (57%) create mode 100644 src/msfstypes/coherent/apcontroller.d.ts create mode 100644 src/msfstypes/coherent/facilities.d.ts create mode 100644 src/msfstypes/index.d.ts create mode 100644 src/msfstypes/js/animation/animation.d.ts create mode 100644 src/msfstypes/js/avionics.d.ts create mode 100644 src/msfstypes/js/buttons.d.ts create mode 100644 src/msfstypes/js/common.d.ts create mode 100644 src/msfstypes/js/datastorage.d.ts create mode 100644 src/msfstypes/js/datavalidator.d.ts create mode 100644 src/msfstypes/js/flight.d.ts create mode 100644 src/msfstypes/js/inputs.d.ts create mode 100644 src/msfstypes/js/leaderboards.d.ts create mode 100644 src/msfstypes/js/netbingmap.d.ts create mode 100644 src/msfstypes/js/padscroll.d.ts create mode 100644 src/msfstypes/js/radionav.d.ts create mode 100644 src/msfstypes/js/services/aircraft.d.ts create mode 100644 src/msfstypes/js/services/checklistmanager.d.ts create mode 100644 src/msfstypes/js/services/community.d.ts create mode 100644 src/msfstypes/js/services/contentmanager.d.ts create mode 100644 src/msfstypes/js/services/controls.d.ts create mode 100644 src/msfstypes/js/services/fuelpayload.d.ts create mode 100644 src/msfstypes/js/services/gamenavlog.d.ts create mode 100644 src/msfstypes/js/services/genericpanelmanager.d.ts create mode 100644 src/msfstypes/js/services/ingamepanelcontrolsviewlistener.d.ts create mode 100644 src/msfstypes/js/services/installationmanager.d.ts create mode 100644 src/msfstypes/js/services/market.d.ts create mode 100644 src/msfstypes/js/services/notifications.d.ts create mode 100644 src/msfstypes/js/services/objective.d.ts create mode 100644 src/msfstypes/js/services/profile.d.ts create mode 100644 src/msfstypes/js/services/raceleaderboard.d.ts create mode 100644 src/msfstypes/js/services/replay.d.ts create mode 100644 src/msfstypes/js/services/rewardscreenmanager.d.ts create mode 100644 src/msfstypes/js/services/toolbarpanels.d.ts create mode 100644 src/msfstypes/js/services/tooltipsmanager.d.ts create mode 100644 src/msfstypes/js/services/weather.d.ts create mode 100644 src/msfstypes/js/simplane.d.ts create mode 100644 src/msfstypes/js/simvar.d.ts create mode 100644 src/msfstypes/js/slider.d.ts create mode 100644 src/msfstypes/js/sortedlist.d.ts create mode 100644 src/msfstypes/js/testsimvar.d.ts create mode 100644 src/msfstypes/js/types.d.ts create mode 100644 src/msfstypes/js/uiresourceelement.d.ts create mode 100644 src/msfstypes/js/wasmsimcanvas.d.ts create mode 100644 src/msfstypes/js/widgetscontainer.d.ts create mode 100644 src/msfstypes/package.json create mode 100644 src/msfstypes/pages/vcockpit/core/vcockpit.d.ts create mode 100644 src/msfstypes/pages/vcockpit/core/vcockpitlogic.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/baseinstrument.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/flightelements/approach.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/flightelements/flightplan.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/flightelements/flightplanmanager.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/flightelements/geocalc.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/flightelements/nearestwaypoint.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/flightelements/runway.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/flightelements/waypoint.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/flightelements/waypointloader.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/utils/datareadmanager.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/utils/radionav.d.ts create mode 100644 src/msfstypes/pages/vcockpit/instruments/shared/utils/xmllogic.d.ts create mode 100644 src/msfstypes/pages/vcockpit/systems/systems.d.ts create mode 100644 src/msfstypes/pages/vcockpit/systems/systems_as1000.d.ts create mode 100644 src/sdk/autopilot/directors/APPitchLvlDirector.ts create mode 100644 src/sdk/components/map/DefaultMapLabeledRingLabel.tsx create mode 100644 src/sdk/components/map/MapLabeledRingLabel.ts create mode 100644 src/sdk/components/map/layers/GenericMapSharedCanvasSubLayer.ts create mode 100644 src/sdk/components/map/layers/MapLabeledRingCanvasSubLayer.tsx create mode 100644 src/sdk/components/map/layers/MapSharedCanvasLayer.tsx create mode 100644 src/sdk/instruments/Accelerometer.ts create mode 100644 src/sdk/instruments/AircraftInertialPublisher.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Assets/Fonts/DejaVuSans-SemiBold.ttf delete mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator.css delete mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator.tsx create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicator.css create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicator.tsx create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicatorDataProvider.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/index.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/CasAlertsBridge.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/Config.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/DefaultConfigFactory.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/LookupTableConfig.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/NumericConfig.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/SpeedConfig.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/index.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000PfdPlugin.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Input/ControlpadHEventHandler.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/AirspeedIndicatorConfig.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/ColorRangeConfig.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/VSpeedBugConfig.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/index.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/Autopilot/AutopilotConfig.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/Autopilot/index.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Controllers/ControlpadInputController.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Controllers/XpdrInputController.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/ArrowControl.css create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/ArrowControl.tsx create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/CourseNumberInput.tsx create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/TimeNumberInput.tsx create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeed.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedConfig.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedGroupConfig.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedUserSettings.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/index.ts create mode 100644 src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/package.json delete mode 100644 src/workingtitle-instruments-g1000/rollup-defs.config.js create mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CharInput/CharInput.tsx create mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CharInput/CharInputSlot.tsx create mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CharInput/index.ts create mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/Keyboard.css create mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/Keyboard.tsx create mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/index.ts create mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcWaypointDialog.css create mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcWaypointDialog.tsx delete mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/rollup-defs.config.js delete mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/rollup-defs.config.js delete mode 100644 src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/rollup-defs.config.js delete mode 100644 src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/rollup-defs.config.js create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsNavBrgSrcMenu.css create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsNavBrgSrcMenu.tsx create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs1Menu.tsx create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs2Menu.css create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs2Menu.tsx create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefsMenu.css create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Config/DisplayUnitConfig.ts create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/FormatInfo.css create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/FormatSwitch.tsx create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/CyclicRadioItem.css create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/CyclicRadioList.css create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/FloatingRadioItem.tsx create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/FloatingRadioList.tsx create mode 100644 src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/WT21DisplayUnitFsInstrument.ts diff --git a/src/garminsdk/autopilot/GarminAPConfig.ts b/src/garminsdk/autopilot/GarminAPConfig.ts index 5a6aa7ec6..24a017290 100644 --- a/src/garminsdk/autopilot/GarminAPConfig.ts +++ b/src/garminsdk/autopilot/GarminAPConfig.ts @@ -1,13 +1,13 @@ import { - APAltCapDirector, APAltDirector, APBackCourseDirector, APConfig, APFLCDirector, APGPDirector, APGSDirector, APHdgDirector, APLateralModes, APLvlDirector, - APNavDirector, APPitchDirector, APRollDirector, APTogaPitchDirector, APValues, APVerticalModes, APVNavPathDirector, APVSDirector, EventBus, - FlightPlanner, LNavDirector, LNavDirectorOptions, NavMath, PlaneDirector, UnitType, VNavManager, VNavPathCalculator + APAltCapDirector, APAltDirector, APBackCourseDirector, APFLCDirector, APGPDirector, APGSDirector, APHdgDirector, APLateralModes, APLvlDirector, APNavDirector, + APPitchDirector, APPitchLvlDirector, APRollDirector, APTogaPitchDirector, APValues, APVerticalModes, APVNavPathDirector, APVSDirector, AutopilotDriverOptions, + EventBus, FlightPlanner, LNavDirector, LNavDirectorOptions, NavMath, PlaneDirector, UnitType, VNavManager, VNavPathCalculator } from '@microsoft/msfs-sdk'; import { GarminObsDirector } from './directors'; +import { GarminAPConfigInterface } from './GarminAPConfigInterface'; import { GarminNavToNavManager } from './GarminNavToNavManager'; import { GarminVNavGuidanceOptions, GarminVNavManager2 } from './GarminVNavManager2'; -import { AutopilotDriverOptions } from '@microsoft/msfs-sdk/autopilot/AutopilotDriver'; /** * Options for configuring a Garmin LNAV director. @@ -65,10 +65,21 @@ export type GarminAPConfigDirectorOptions = { hdgTurnReversalThreshold?: number; }; +/** + * Options for configuring Garmin autopilots. + */ +export type GarminAPConfigOptions = GarminAPConfigDirectorOptions & { + /** + * Whether the autopilot should use mach number calculated from the impact pressure derived from indicated airspeed + * and ambient pressure instead of the true mach number. Defaults to `false`. + */ + useIndicatedMach?: boolean; +} + /** * A Garmin Autopilot Configuration. */ -export class GarminAPConfig implements APConfig { +export class GarminAPConfig implements GarminAPConfigInterface { /** The default commanded pitch angle rate, in degrees per second. */ public static readonly DEFAULT_PITCH_RATE = 5; @@ -93,6 +104,12 @@ export class GarminAPConfig implements APConfig { public autopilotDriverOptions: AutopilotDriverOptions; + /** + * Whether the autopilot should use mach number calculated from the impact pressure derived from indicated airspeed + * and ambient pressure instead of the true mach number. + */ + public readonly useIndicatedMach: boolean; + /** Options for the LNAV director. */ private readonly lnavOptions: Partial>; @@ -120,13 +137,15 @@ export class GarminAPConfig implements APConfig { private readonly bus: EventBus, private readonly flightPlanner: FlightPlanner, private readonly verticalPathCalculator: VNavPathCalculator, - options?: Readonly + options?: Readonly ) { this.autopilotDriverOptions = { pitchServoRate: options?.defaultPitchRate ?? GarminAPConfig.DEFAULT_PITCH_RATE, bankServoRate: options?.defaultBankRate ?? GarminAPConfig.DEFAULT_BANK_RATE }; + this.useIndicatedMach = options?.useIndicatedMach ?? false; + this.lnavOptions = { ...options?.lnavOptions }; this.vnavOptions = { ...options?.vnavOptions }; this.rollMinBankAngle = options?.rollMinBankAngle ?? GarminAPConfig.DEFAULT_ROLL_MIN_BANK_ANGLE; @@ -157,6 +176,11 @@ export class GarminAPConfig implements APConfig { return new APLvlDirector(this.bus, apValues); } + /** @inheritdoc */ + public createPitchLevelerDirector(apValues: APValues): APPitchLvlDirector { + return new APPitchLvlDirector(apValues); + } + /** @inheritdoc */ public createGpssDirector(apValues: APValues): LNavDirector { const maxBankAngle = (): number => apValues.maxBankId.get() === 1 ? Math.min(this.lnavMaxBankAngle, this.lowBankAngle) : this.lnavMaxBankAngle; @@ -214,7 +238,7 @@ export class GarminAPConfig implements APConfig { /** @inheritdoc */ public createFlcDirector(apValues: APValues): APFLCDirector { - return new APFLCDirector(apValues); + return new APFLCDirector(apValues, { useIndicatedMach: this.useIndicatedMach }); } /** @inheritdoc */ diff --git a/src/garminsdk/autopilot/GarminAPConfigInterface.ts b/src/garminsdk/autopilot/GarminAPConfigInterface.ts new file mode 100644 index 000000000..ccd6c1748 --- /dev/null +++ b/src/garminsdk/autopilot/GarminAPConfigInterface.ts @@ -0,0 +1,12 @@ +import { APConfig } from '@microsoft/msfs-sdk'; + +/** + * A Garmin Autopilot Configuration. + */ +export interface GarminAPConfigInterface extends APConfig { + /** + * Whether the autopilot should use mach number calculated from the impact pressure derived from indicated airspeed + * and ambient pressure instead of the true mach number. + */ + readonly useIndicatedMach: boolean; +} \ No newline at end of file diff --git a/src/garminsdk/autopilot/GarminAPStateManager.ts b/src/garminsdk/autopilot/GarminAPStateManager.ts index 56df3bf39..6cd34beab 100644 --- a/src/garminsdk/autopilot/GarminAPStateManager.ts +++ b/src/garminsdk/autopilot/GarminAPStateManager.ts @@ -178,6 +178,15 @@ export class GarminAPStateManager extends APStateManager { case 'AP_WING_LEVELER_OFF': this.sendApModeEvent(APModeType.LATERAL, APLateralModes.LEVEL, false); break; + case 'AP_PITCH_LEVELER': + this.sendApModeEvent(APModeType.VERTICAL, APVerticalModes.LEVEL); + break; + case 'AP_PITCH_LEVELER_ON': + this.sendApModeEvent(APModeType.VERTICAL, APVerticalModes.LEVEL, true); + break; + case 'AP_PITCH_LEVELER_OFF': + this.sendApModeEvent(APModeType.VERTICAL, APVerticalModes.LEVEL, false); + break; case 'AP_PANEL_VS_HOLD': case 'AP_VS_HOLD': this.sendApModeEvent(APModeType.VERTICAL, APVerticalModes.VS); diff --git a/src/garminsdk/autopilot/GarminAutopilot.ts b/src/garminsdk/autopilot/GarminAutopilot.ts index b1d522ac2..0f1b7fa41 100644 --- a/src/garminsdk/autopilot/GarminAutopilot.ts +++ b/src/garminsdk/autopilot/GarminAutopilot.ts @@ -1,11 +1,13 @@ import { - AdcEvents, AltitudeSelectManager, AltitudeSelectManagerOptions, APAltitudeModes, APConfig, APEvents, APLateralModes, APStateManager, - APVerticalModes, Autopilot, ConsumerSubject, ConsumerValue, DirectorState, EventBus, FlightPlanner, MappedSubject, MetricAltitudeSettingsManager, - MinimumsMode, ObjectSubject, SetSubject, SimVarValueType, UnitType, VNavAltCaptureType, VNavState + AdcEvents, AltitudeSelectManager, AltitudeSelectManagerOptions, APAltitudeModes, APEvents, APLateralModes, APStateManager, + APVerticalModes, Autopilot, ConsumerSubject, ConsumerValue, DirectorState, EventBus, FlightPlanner, MappedSubject, + MetricAltitudeSettingsManager, MinimumsMode, ObjectSubject, SetSubject, SimVarValueType, UnitType, VNavAltCaptureType, + VNavState } from '@microsoft/msfs-sdk'; -import { FmaData, FmaDataEvents, FmaVNavState } from './FmaData'; import { MinimumsDataProvider } from '../minimums/MinimumsDataProvider'; +import { FmaData, FmaDataEvents, FmaVNavState } from './FmaData'; +import { GarminAPConfigInterface } from './GarminAPConfigInterface'; import { GarminVNavManager2 } from './GarminVNavManager2'; /** @@ -50,7 +52,7 @@ export type GarminAutopilotOptions = { /** * A Garmin autopilot. */ -export class GarminAutopilot extends Autopilot { +export class GarminAutopilot extends Autopilot { protected static readonly ALT_SELECT_OPTIONS_DEFAULT: AltitudeSelectManagerOptions = { supportMetric: true, minValue: UnitType.FOOT.createNumber(-1000), @@ -101,7 +103,7 @@ export class GarminAutopilot extends Autopilot { constructor( bus: EventBus, flightPlanner: FlightPlanner, - config: APConfig, + config: GarminAPConfigInterface, stateManager: APStateManager, options?: Readonly ) { @@ -169,7 +171,7 @@ export class GarminAutopilot extends Autopilot { // Whenever we switch between mach and IAS hold and we are in manual speed mode, we need to set the value to which // we are switching to be equal to the value we are switching from. - this.machToKias.setConsumer(sub.on('mach_to_kias_factor_1')); + this.machToKias.setConsumer(sub.on(this.config.useIndicatedMach ? 'indicated_mach_to_kias_factor_1' : 'mach_to_kias_factor_1')); this.selSpeedIsMach.setConsumer(sub.on('ap_selected_speed_is_mach')); const speedIsMachSub = this.selSpeedIsMach.sub(isMach => { diff --git a/src/garminsdk/autopilot/GarminNavToNavManager.ts b/src/garminsdk/autopilot/GarminNavToNavManager.ts index 95ec9f0ed..f5d4433b0 100644 --- a/src/garminsdk/autopilot/GarminNavToNavManager.ts +++ b/src/garminsdk/autopilot/GarminNavToNavManager.ts @@ -22,7 +22,8 @@ export class GarminNavToNavManager implements NavToNavManager { rnavTypeFlags: RnavTypeFlags.None, isCircling: false, isVtf: false, - referenceFacility: null + referenceFacility: null, + runway: null }; private nav1Frequency = 0; diff --git a/src/garminsdk/autopilot/GarminVNavManager.ts b/src/garminsdk/autopilot/GarminVNavManager.ts index bff3bcc66..4ea269d5f 100644 --- a/src/garminsdk/autopilot/GarminVNavManager.ts +++ b/src/garminsdk/autopilot/GarminVNavManager.ts @@ -32,7 +32,8 @@ export class GarminVNavManager implements VNavManager { rnavTypeFlags: RnavTypeFlags.None, isCircling: false, isVtf: false, - referenceFacility: null + referenceFacility: null, + runway: null }, FmsUtils.approachDetailsEquals); private readonly gpAvailable = ConsumerSubject.create(null, false); diff --git a/src/garminsdk/autopilot/GarminVNavManager2.ts b/src/garminsdk/autopilot/GarminVNavManager2.ts index 99ac99c52..05dd50686 100644 --- a/src/garminsdk/autopilot/GarminVNavManager2.ts +++ b/src/garminsdk/autopilot/GarminVNavManager2.ts @@ -100,7 +100,8 @@ export class GarminVNavManager2 implements VNavManager { rnavTypeFlags: RnavTypeFlags.None, isCircling: false, isVtf: false, - referenceFacility: null + referenceFacility: null, + runway: null }, FmsUtils.approachDetailsEquals); private readonly isApproachLoc = this.approachDetails.map(details => { switch (details.type) { diff --git a/src/garminsdk/autopilot/index.ts b/src/garminsdk/autopilot/index.ts index 2d9b06501..e683975f6 100644 --- a/src/garminsdk/autopilot/index.ts +++ b/src/garminsdk/autopilot/index.ts @@ -1,5 +1,6 @@ export * from './FmaData'; export * from './GarminAPConfig'; +export * from './GarminAPConfigInterface'; export * from './GarminAPStateManager'; export * from './GarminAutopilot'; export * from './GarminGoAroundManager'; diff --git a/src/garminsdk/components/cas/CAS.tsx b/src/garminsdk/components/cas/CAS.tsx index dcc8e6571..223249e6f 100644 --- a/src/garminsdk/components/cas/CAS.tsx +++ b/src/garminsdk/components/cas/CAS.tsx @@ -165,9 +165,11 @@ export class CASDisplay extends DisplayComponent { private handleMessageChanged(idx: number, type: SubscribableArrayEventType.Added | SubscribableArrayEventType.Removed, item: CasActiveMessage): void { switch (type) { case SubscribableArrayEventType.Added: - this.addAnnunciation(idx, item); break; + this.addAnnunciation(idx, item); + break; case SubscribableArrayEventType.Removed: - this.removeAnnunciation(idx, item); break; + this.removeAnnunciation(idx, item); + break; } } @@ -183,13 +185,17 @@ export class CASDisplay extends DisplayComponent { if (this.props.alertCounts !== undefined) { switch (item.priority) { case AnnunciationType.Warning: - this.props.alertCounts.set('numWarning', this.props.alertCounts.get().numWarning + 1); break; + this.props.alertCounts.set('numWarning', this.props.alertCounts.get().numWarning + 1); + break; case AnnunciationType.Caution: - this.props.alertCounts.set('numCaution', this.props.alertCounts.get().numCaution + 1); break; + this.props.alertCounts.set('numCaution', this.props.alertCounts.get().numCaution + 1); + break; case AnnunciationType.Advisory: - this.props.alertCounts.set('numAdvisory', this.props.alertCounts.get().numAdvisory + 1); break; + this.props.alertCounts.set('numAdvisory', this.props.alertCounts.get().numAdvisory + 1); + break; case AnnunciationType.SafeOp: - this.props.alertCounts.set('numSafeOp', this.props.alertCounts.get().numSafeOp + 1); break; + this.props.alertCounts.set('numSafeOp', this.props.alertCounts.get().numSafeOp + 1); + break; } this.updateAlertCounts(); } @@ -217,13 +223,17 @@ export class CASDisplay extends DisplayComponent { if (this.props.alertCounts !== undefined) { switch (item.priority) { case AnnunciationType.Warning: - this.props.alertCounts.set('numWarning', this.props.alertCounts.get().numWarning - 1); break; + this.props.alertCounts.set('numWarning', this.props.alertCounts.get().numWarning - 1); + break; case AnnunciationType.Caution: - this.props.alertCounts.set('numCaution', this.props.alertCounts.get().numCaution - 1); break; + this.props.alertCounts.set('numCaution', this.props.alertCounts.get().numCaution - 1); + break; case AnnunciationType.Advisory: - this.props.alertCounts.set('numAdvisory', this.props.alertCounts.get().numAdvisory - 1); break; + this.props.alertCounts.set('numAdvisory', this.props.alertCounts.get().numAdvisory - 1); + break; case AnnunciationType.SafeOp: - this.props.alertCounts.set('numSafeOp', this.props.alertCounts.get().numSafeOp - 1); break; + this.props.alertCounts.set('numSafeOp', this.props.alertCounts.get().numSafeOp - 1); + break; } this.updateAlertCounts(); } diff --git a/src/garminsdk/components/common/BearingDisplay.tsx b/src/garminsdk/components/common/BearingDisplay.tsx index 47f8a2a76..1c015e8e0 100644 --- a/src/garminsdk/components/common/BearingDisplay.tsx +++ b/src/garminsdk/components/common/BearingDisplay.tsx @@ -1,95 +1,152 @@ import { - AbstractNumberUnitDisplay, AbstractNumberUnitDisplayProps, FSComponent, NavAngleUnit, NavAngleUnitFamily, NumberUnitInterface, Subject, SubscribableSet, Unit, VNode + AbstractNumberUnitDisplay, AbstractNumberUnitDisplayProps, FSComponent, NavAngleUnit, NavAngleUnitFamily, + NumberUnitInterface, Subject, Subscribable, SubscribableSet, ToggleableClassNameRecord, Unit, VNode } from '@microsoft/msfs-sdk'; /** * Component props for BearingDisplay. */ -export interface BearingDisplayProps extends AbstractNumberUnitDisplayProps { +export interface BearingDisplayProps extends Omit, 'value' | 'displayUnit'> { + /** The {@link NumberUnitInterface} value to display, or a subscribable which provides it. */ + value: NumberUnitInterface | Subscribable>; + + /** + * The unit type in which to display the value, or a subscribable which provides it. If the unit is `null`, then the + * native type of the value is used instead. + */ + displayUnit: NavAngleUnit | null | Subscribable; + /** A function which formats numbers. */ formatter: (number: number) => string; - /** Whether to display 360 in place of 0. True by default. */ + /** + * A function which formats units. The formatted unit text should be written to the 2-tuple passed to the `out` + * parameter, as `[bigText, smallText]`. `bigText` and `smallText` will be rendered into separate `` elements + * representing the big and small components of the rendered unit text, respectively. If not defined, then units + * will be formatted such that `bigText` is always the degree symbol (°) and `smallText` is empty for magnetic + * bearing or `'T'` for true bearing. + */ + unitFormatter?: (out: [string, string], unit: NavAngleUnit, number: number) => void; + + /** Whether to display `'360'` in place of `'0'`. Defaults to `true`. */ use360?: boolean; - /** Whether to hide the ° symbol when value is NaN. False by default. */ + /** Whether to hide the unit text when the displayed value is equal to `NaN`. Defaults to `false`. */ hideDegreeSymbolWhenNan?: boolean; /** CSS class(es) to add to the root of the bearing display component. */ - class?: string | SubscribableSet; + class?: string | SubscribableSet | ToggleableClassNameRecord; } /** * Displays a bearing value. */ export class BearingDisplay extends AbstractNumberUnitDisplay { - private readonly unitTextSmallRef = FSComponent.createRef(); - private readonly bearingUnitRef = FSComponent.createRef(); - private readonly numberTextSub = Subject.create(''); - private readonly unitTextSmallSub = Subject.create(''); + /** + * A function which formats units to default text for BearingDisplay. + * @param out The 2-tuple to which to write the formatted text, as `[bigText, smallText]`. + * @param unit The unit to format. + */ + public static readonly DEFAULT_UNIT_FORMATTER = (out: [string, string], unit: NavAngleUnit): void => { + out[0] = '°'; + out[1] = unit.isMagnetic() ? '' : 'T'; + }; - /** @inheritdoc */ - constructor(props: BearingDisplayProps) { - super(props); + private static readonly unitTextCache: [string, string] = ['', '']; - this.props.use360 ??= true; - } + private readonly unitFormatter = this.props.unitFormatter ?? BearingDisplay.DEFAULT_UNIT_FORMATTER; - /** @inheritdoc */ - public onAfterRender(): void { - super.onAfterRender(); + private readonly unitTextBigDisplay = Subject.create(''); + private readonly unitTextSmallDisplay = Subject.create(''); - // We have to hide the "small" unit text when empty because an empty string will get rendered as a space. - this.unitTextSmallSub.sub((text): void => { this.unitTextSmallRef.instance.style.display = text === '' ? 'none' : ''; }, true); - } + private readonly numberText = Subject.create(''); + private readonly unitTextBig = Subject.create(''); + private readonly unitTextSmall = Subject.create(''); /** @inheritdoc */ protected onValueChanged(value: NumberUnitInterface): void { - this.setDisplay(value, this.displayUnit.get()); + let displayUnit = this.displayUnit.get(); + if (!displayUnit || !value.unit.canConvert(displayUnit)) { + displayUnit = value.unit; + } + + const numberValue = value.asUnit(displayUnit); + + this.updateNumberText(numberValue); + this.updateUnitText(numberValue, displayUnit as NavAngleUnit); + + if (this.props.hideDegreeSymbolWhenNan === true) { + this.updateUnitTextVisibility(numberValue); + } } /** @inheritdoc */ protected onDisplayUnitChanged(displayUnit: Unit | null): void { - this.setDisplay(this.value.get(), displayUnit); - } - - /** - * Displays this component's current value. - * @param value The current value. - * @param displayUnit The current display unit. - */ - private setDisplay(value: NumberUnitInterface, displayUnit: Unit | null): void { + const value = this.value.get(); if (!displayUnit || !value.unit.canConvert(displayUnit)) { displayUnit = value.unit; } - const number = value.asUnit(displayUnit); - let numberText = this.props.formatter(number); - if (this.props.use360 && parseFloat(numberText) === 0) { + const numberValue = value.asUnit(displayUnit); + + this.updateNumberText(numberValue); + this.updateUnitText(numberValue, displayUnit as NavAngleUnit); + this.updateUnitTextVisibility(numberValue); + } + + /** + * Updates this component's displayed number text. + * @param numberValue The numeric value to display. + */ + private updateNumberText(numberValue: number): void { + let numberText = this.props.formatter(numberValue); + if (this.props.use360 !== false && parseFloat(numberText) === 0) { numberText = this.props.formatter(360); } - this.numberTextSub.set(numberText); + this.numberText.set(numberText); + } + /** + * Updates this component's displayed unit text. + * @param numberValue The numeric value to display. + * @param displayUnit The unit type in which to display the value. + */ + private updateUnitText(numberValue: number, displayUnit: NavAngleUnit): void { + BearingDisplay.unitTextCache[0] = ''; + BearingDisplay.unitTextCache[1] = ''; + + this.unitFormatter(BearingDisplay.unitTextCache, displayUnit, numberValue); + this.unitTextBig.set(BearingDisplay.unitTextCache[0]); + this.unitTextSmall.set(BearingDisplay.unitTextCache[1]); + } + + /** + * Updates whether this component's unit text spans are visible. + * @param numberValue The numeric value displayed by this component. + */ + private updateUnitTextVisibility(numberValue: number): void { if (this.props.hideDegreeSymbolWhenNan === true) { - this.bearingUnitRef.instance.style.display = isNaN(number) ? 'none' : 'inline'; + if (isNaN(numberValue)) { + this.unitTextBigDisplay.set('none'); + this.unitTextSmallDisplay.set('none'); + return; + } } - if (this.props.hideDegreeSymbolWhenNan === true && isNaN(number)) { - this.unitTextSmallSub.set(''); - } else { - this.unitTextSmallSub.set((displayUnit as NavAngleUnit).isMagnetic() ? '' : 'T'); - } + // We have to hide the unit text when empty because an empty string will get rendered as a space. + this.unitTextBigDisplay.set(this.unitTextBig.get() === '' ? 'none' : ''); + this.unitTextSmallDisplay.set(this.unitTextSmall.get() === '' ? 'none' : ''); } /** @inheritdoc */ public render(): VNode { return (
- {this.numberTextSub} - ° - {this.unitTextSmallSub} + {this.numberText} + {this.unitTextBig} + {this.unitTextSmall}
); } diff --git a/src/garminsdk/components/common/LatLonDisplay.tsx b/src/garminsdk/components/common/LatLonDisplay.tsx index 18f46c4b1..73355d71c 100644 --- a/src/garminsdk/components/common/LatLonDisplay.tsx +++ b/src/garminsdk/components/common/LatLonDisplay.tsx @@ -10,6 +10,9 @@ export enum LatLonDisplayFormat { /** HDDD° MM.MM' */ HDDD_MMmm = 'HDDD° MM.MM\'', + /** HDDD° MM.MMM' */ + HDDD_MMmmm = 'HDDD° MM.MMM\'', + /** HDDD° MM' SS.S */ HDDD_MM_SSs = 'HDDD° MM\' SS.S' } @@ -24,6 +27,9 @@ export interface LatLonDisplayProps extends ComponentProps { /** The format to use to display the coordinates. */ format: LatLonDisplayFormat | Subscribable; + /** Whether to split the prefix into a separate `span` element within the `div.g-latlon-coord`. Defaults to `false`. */ + splitPrefix?: boolean; + /** CSS class(es) to add to the component's root element. */ class?: string | SubscribableSet; } @@ -34,11 +40,13 @@ export interface LatLonDisplayProps extends ComponentProps { export class LatLonDisplay extends DisplayComponent { private static readonly LAT_FORMATTERS = { [LatLonDisplayFormat.HDDD_MMmm]: DmsFormatter2.create('{+[N]-[S]} {dd}°{mm.mm}\'', UnitType.ARC_SEC, 0.6, 'N __°__.__\''), + [LatLonDisplayFormat.HDDD_MMmmm]: DmsFormatter2.create('{+[N]-[S]} {dd}°{mm.mmm}\'', UnitType.ARC_SEC, 0.6, 'N __°__.___\''), [LatLonDisplayFormat.HDDD_MM_SSs]: DmsFormatter2.create('{+[N]-[S]} {dd}°{mm}\'{ss.s}"', UnitType.ARC_SEC, 0.1, 'N __°__\'__._"'), }; private static readonly LON_FORMATTERS = { [LatLonDisplayFormat.HDDD_MMmm]: DmsFormatter2.create('{+[E]-[W]}{ddd}°{mm.mm}\'', UnitType.ARC_SEC, 0.6, 'E___°__.__\''), + [LatLonDisplayFormat.HDDD_MMmmm]: DmsFormatter2.create('{+[E]-[W]}{ddd}°{mm.mmm}\'', UnitType.ARC_SEC, 0.6, 'E___°__.___\''), [LatLonDisplayFormat.HDDD_MM_SSs]: DmsFormatter2.create('{+[E]-[W]}{ddd}°{mm}\'{ss.s}"', UnitType.ARC_SEC, 0.1, 'E___°__\'__._"'), }; @@ -51,7 +59,12 @@ export class LatLonDisplay extends DisplayComponent { ); private readonly latText = Subject.create(''); + private readonly latPrefixText = this.props.splitPrefix ? this.latText.map((it) => it[0]) : undefined; + private readonly latNumberText = this.props.splitPrefix ? this.latText.map((it) => it.substring(2) ?? '') : undefined; + private readonly lonText = Subject.create(''); + private readonly lonPrefixText = this.props.splitPrefix ? this.lonText.map((it) => it[0]) : undefined; + private readonly lonNumberText = this.props.splitPrefix ? this.lonText.map((it) => it.substring(1) ?? '') : undefined; /** @inheritdoc */ public onAfterRender(): void { @@ -68,8 +81,23 @@ export class LatLonDisplay extends DisplayComponent { public render(): VNode { return (
-
{this.latText}
-
{this.lonText}
+ {this.props.splitPrefix ? ( + <> +
+ {this.latPrefixText} + {this.latNumberText} +
+
+ {this.lonPrefixText} + {this.lonNumberText} +
+ + ) : ( + <> +
{this.latText}
+
{this.lonText}
+ + )}
); } diff --git a/src/garminsdk/components/common/NumberUnitDisplay.tsx b/src/garminsdk/components/common/NumberUnitDisplay.tsx index 073ead378..7c918a003 100644 --- a/src/garminsdk/components/common/NumberUnitDisplay.tsx +++ b/src/garminsdk/components/common/NumberUnitDisplay.tsx @@ -1,4 +1,7 @@ -import { AbstractNumberUnitDisplay, AbstractNumberUnitDisplayProps, FSComponent, NumberUnitInterface, Subject, SubscribableSet, Unit, VNode } from '@microsoft/msfs-sdk'; +import { + AbstractNumberUnitDisplay, AbstractNumberUnitDisplayProps, FSComponent, NumberUnitInterface, Subject, SubscribableSet, + ToggleableClassNameRecord, Unit, VNode +} from '@microsoft/msfs-sdk'; import { UnitFormatter } from '../../graphics/text/UnitFormatter'; @@ -9,70 +12,146 @@ export interface NumberUnitDisplayProps extends AbstractNumber /** A function which formats numbers. */ formatter: (number: number) => string; + /** + * A function which formats units. The formatted unit text should be written to the 2-tuple passed to the `out` + * parameter, as `[bigText, smallText]`. `bigText` and `smallText` will be rendered into separate `` elements + * representing the big and small components of the rendered unit text, respectively. If not defined, then units + * will be formatted based on the text generated by the {@link UnitFormatter} class. + */ + unitFormatter?: (out: [string, string], unit: Unit, number: number) => void; + + /** Whether to hide the unit text when the displayed value is equal to `NaN`. Defaults to `false`. */ + hideUnitWhenNaN?: boolean; + /** CSS class(es) to add to the root of the icon component. */ - class?: string | SubscribableSet; + class?: string | SubscribableSet | ToggleableClassNameRecord; } /** * A component which displays a number with units. */ export class NumberUnitDisplay extends AbstractNumberUnitDisplay> { - private static readonly UNIT_FORMATTER = UnitFormatter.create(); - private readonly unitTextBigRef = FSComponent.createRef(); + // We create our own map instead of using UnitFormatter.create() so that we don't have to generate new big and small + // text substrings with every call to the default unit formatter function. + private static readonly DEFAULT_UNIT_TEXT_MAP = NumberUnitDisplay.createDefaultUnitTextMap(); + + /** + * A function which formats units to default text for NumberUnitDisplay. + * @param out The 2-tuple to which to write the formatted text, as `[bigText, smallText]`. + * @param unit The unit to format. + */ + public static readonly DEFAULT_UNIT_FORMATTER = (out: [string, string], unit: Unit): void => { + const text = NumberUnitDisplay.DEFAULT_UNIT_TEXT_MAP[unit.family]?.[unit.name]; + + if (text) { + out[0] = text[0]; + out[1] = text[1]; + } + }; + + private static readonly unitTextCache: [string, string] = ['', '']; + + private readonly unitFormatter = this.props.unitFormatter ?? NumberUnitDisplay.DEFAULT_UNIT_FORMATTER; + + private readonly unitTextBigDisplay = Subject.create(''); + private readonly unitTextSmallDisplay = Subject.create(''); private readonly numberText = Subject.create(''); private readonly unitTextBig = Subject.create(''); private readonly unitTextSmall = Subject.create(''); - /** @inheritdoc */ - public onAfterRender(): void { - super.onAfterRender(); - - // We have to hide the "big" unit text when empty because an empty string will get rendered as a space. - this.unitTextBig.sub((text): void => { this.unitTextBigRef.instance.style.display = text === '' ? 'none' : ''; }, true); - } - /** @inheritdoc */ protected onValueChanged(value: NumberUnitInterface): void { - this.setDisplay(value, this.displayUnit.get()); + this.updateDisplay(value, this.displayUnit.get()); } /** @inheritdoc */ protected onDisplayUnitChanged(displayUnit: Unit | null): void { - this.setDisplay(this.value.get(), displayUnit); + this.updateDisplay(this.value.get(), displayUnit); } /** - * Displays this component's current value. - * @param value The current value. - * @param displayUnit The current display unit. + * Updates this component's displayed number and unit text. + * @param value The value to display. + * @param displayUnit The unit type in which to display the value, or `null` if the value should be displayed in its + * native unit type. */ - private setDisplay(value: NumberUnitInterface, displayUnit: Unit | null): void { + private updateDisplay(value: NumberUnitInterface, displayUnit: Unit | null): void { if (!displayUnit || !value.unit.canConvert(displayUnit)) { displayUnit = value.unit; } - const numberText = this.props.formatter(value.asUnit(displayUnit)); + const numberValue = value.asUnit(displayUnit); + + const numberText = this.props.formatter(numberValue); this.numberText.set(numberText); - const unitText = NumberUnitDisplay.UNIT_FORMATTER(displayUnit); + NumberUnitDisplay.unitTextCache[0] = ''; + NumberUnitDisplay.unitTextCache[1] = ''; - if (unitText[0] === '°') { - this.unitTextBig.set('°'); - this.unitTextSmall.set(unitText.substring(1)); - } else { - this.unitTextBig.set(''); - this.unitTextSmall.set(unitText); + this.unitFormatter(NumberUnitDisplay.unitTextCache, displayUnit, numberValue); + this.unitTextBig.set(NumberUnitDisplay.unitTextCache[0]); + this.unitTextSmall.set(NumberUnitDisplay.unitTextCache[1]); + + this.updateUnitTextVisibility(numberValue, NumberUnitDisplay.unitTextCache[0], NumberUnitDisplay.unitTextCache[1]); + } + + /** + * Updates whether this component's unit text spans are visible. + * @param numberValue The numeric value displayed by this component. + * @param unitTextBig The text to display in the big text span. + * @param unitTextSmall The text to display in the small text span. + */ + private updateUnitTextVisibility(numberValue: number, unitTextBig: string, unitTextSmall: string): void { + if (this.props.hideUnitWhenNaN === true && isNaN(numberValue)) { + this.unitTextBigDisplay.set('none'); + this.unitTextSmallDisplay.set('none'); + return; } + + // We have to hide the unit text when empty because an empty string will get rendered as a space. + this.unitTextBigDisplay.set(unitTextBig === '' ? 'none' : ''); + this.unitTextSmallDisplay.set(unitTextSmall === '' ? 'none' : ''); } /** @inheritdoc */ public render(): VNode { return (
- {this.numberText}{this.unitTextBig}{this.unitTextSmall} + {this.numberText} + {this.unitTextBig} + {this.unitTextSmall}
); } + + /** + * Creates the default mapping from unit to displayed text. + * @returns The default mapping from unit to displayed text. + */ + private static createDefaultUnitTextMap(): Partial>>> { + const originalMap = UnitFormatter.getUnitTextMap(); + + const map = {} as Record>; + for (const family in originalMap) { + const nameMap = map[family] = {} as Record; + + const originalNameMap = originalMap[family] as Readonly>>; + for (const name in originalNameMap) { + const text = nameMap[name] = ['', '']; + + const originalText = originalNameMap[name] as string; + + if (originalText[0] === '°') { + text[0] = '°'; + text[1] = originalText.substring(1); + } else { + text[1] = originalText; + } + } + } + + return map; + } } \ No newline at end of file diff --git a/src/garminsdk/components/common/TimeDisplay.tsx b/src/garminsdk/components/common/TimeDisplay.tsx index ab857ea4f..7d8a24250 100644 --- a/src/garminsdk/components/common/TimeDisplay.tsx +++ b/src/garminsdk/components/common/TimeDisplay.tsx @@ -1,5 +1,6 @@ import { - ComponentProps, DisplayComponent, FSComponent, MappedSubscribable, Subject, Subscribable, SubscribableMapFunctions, SubscribableSet, Subscription, VNode + ComponentProps, DisplayComponent, FSComponent, MappedSubscribable, Subject, Subscribable, SubscribableMapFunctions, + SubscribableSet, SubscribableUtils, Subscription, ToggleableClassNameRecord, VNode } from '@microsoft/msfs-sdk'; /** @@ -27,8 +28,19 @@ export interface TimeDisplayProps extends ComponentProps { /** The local time offset, in milliseconds, or a subscribable which provides it. */ localOffset: number | Subscribable; + /** + * A function which formats suffixes to append to the displayed time. If not defined, then the suffix will be + * will be formatted as `'UTC'` if the display format is {@link TimeDisplayFormat.UTC}, `'LCL'` if the display format + * is {@link TimeDisplayFormat.Local24}, and either `'AM'` or `'PM'` if the display format is + * {@link TimeDisplayFormat.Local12}. + */ + suffixFormatter?: (format: TimeDisplayFormat, isAm: boolean) => string; + + /** Whether to hide the suffix when the displayed time is equal to `NaN`. Defaults to `false`. */ + hideSuffixWhenNaN?: boolean; + /** CSS class(es) to apply to the root of the component. */ - class?: string | SubscribableSet; + class?: string | SubscribableSet | ToggleableClassNameRecord; } /** @@ -37,17 +49,35 @@ export interface TimeDisplayProps extends ComponentProps { export class TimeDisplay extends DisplayComponent { private static readonly SECOND_PRECISION_MAP = SubscribableMapFunctions.withPrecision(1000); + private static readonly HIDE_UNIT_TEXT_PIPE = (text: string): string => text === '' ? 'none' : ''; + + /** + * A function which formats suffixes for TimeDisplay. + * @param format The current format used to display the time. + * @param isAm Whether or not the current time is AM. + * @returns The suffix to append to the displayed time. + */ + public static readonly DEFAULT_SUFFIX_FORMATTER = (format: TimeDisplayFormat, isAm: boolean): string => { + if (format === TimeDisplayFormat.UTC) { + return 'UTC'; + } else if (format === TimeDisplayFormat.Local24) { + return 'LCL'; + } else { + return isAm ? 'AM' : 'PM'; + } + }; + private readonly timeSeconds = typeof this.props.time === 'object' ? (this.timeSub = this.props.time.map(TimeDisplay.SECOND_PRECISION_MAP)) : Subject.create(TimeDisplay.SECOND_PRECISION_MAP(this.props.time)); - private readonly format = typeof this.props.format === 'object' - ? this.props.format - : Subject.create(this.props.format); + private readonly format = SubscribableUtils.toSubscribable(this.props.format, true); + + private readonly localOffset = SubscribableUtils.toSubscribable(this.props.localOffset, true); - private readonly localOffset = typeof this.props.localOffset === 'object' - ? this.props.localOffset - : Subject.create(this.props.localOffset); + private readonly suffixFormatter = this.props.suffixFormatter ?? TimeDisplay.DEFAULT_SUFFIX_FORMATTER; + + private readonly suffixDisplay = Subject.create(''); private readonly date = new Date(); @@ -67,6 +97,9 @@ export class TimeDisplay extends DisplayComponent { this.formatSub = this.format.sub(this.updateHandler); this.localOffsetSub = this.localOffset.sub(this.updateHandler); this.timeSeconds.sub(this.updateHandler, true); + + // We have to hide the suffix text when empty because an empty string will get rendered as a space. + this.suffixText.pipe(this.suffixDisplay, TimeDisplay.HIDE_UNIT_TEXT_PIPE); } /** @@ -82,6 +115,8 @@ export class TimeDisplay extends DisplayComponent { this.hourText.set('__'); this.minText.set('__'); this.secText.set('__'); + + this.suffixText.set(this.props.hideSuffixWhenNaN ? '' : this.getSuffix(format, isAm)); } else { const offset = format === TimeDisplayFormat.UTC ? 0 : this.localOffset.get(); @@ -100,25 +135,19 @@ export class TimeDisplay extends DisplayComponent { this.minText.set(this.date.getUTCMinutes().toString().padStart(2, '0')); this.secText.set(this.date.getUTCSeconds().toString().padStart(2, '0')); - } - this.suffixText.set(this.getSuffix(format, isAm)); + this.suffixText.set(this.getSuffix(format, isAm)); + } } /** * Gets the suffix to append to the time display. * @param format The format of the time display. - * @param isAm Whether or not the current time is AM or PM. + * @param isAm Whether or not the current time is AM. * @returns The time display suffix. */ protected getSuffix(format: TimeDisplayFormat, isAm: boolean): string { - if (format === TimeDisplayFormat.UTC) { - return 'UTC'; - } else if (format === TimeDisplayFormat.Local24) { - return 'LCL'; - } else { - return isAm ? 'AM' : 'PM'; - } + return this.suffixFormatter(format, isAm); } /** @inheritdoc */ @@ -128,7 +157,7 @@ export class TimeDisplay extends DisplayComponent { {this.hourText} :{this.minText} :{this.secText} - {this.suffixText} + {this.suffixText} ); } diff --git a/src/garminsdk/components/list/ScrollList.tsx b/src/garminsdk/components/list/ScrollList.tsx index d00e35557..0e9a1c85c 100644 --- a/src/garminsdk/components/list/ScrollList.tsx +++ b/src/garminsdk/components/list/ScrollList.tsx @@ -1,5 +1,5 @@ import { - ComponentProps, DisplayComponent, FSComponent, MappedSubject, MappedSubscribable, MathUtils, ReadonlyFloat64Array, + ComponentProps, DisplayComponent, FSComponent, MappedSubject, MappedSubscribable, MathUtils, MutableSubscribable, ReadonlyFloat64Array, SetSubject, Subject, Subscribable, SubscribableMapFunctions, SubscribableSet, SubscribableUtils, Subscription, ToggleableClassNameRecord, Vec2Math, Vec2Subject, VNode, } from '@microsoft/msfs-sdk'; @@ -59,18 +59,23 @@ export class ScrollList

extends Dis protected readonly translatableRef = FSComponent.createRef(); protected readonly itemsContainerRef = FSComponent.createRef(); - protected readonly listItemLengthPx = SubscribableUtils.toSubscribable(this.props.listItemLengthPx, true); - protected readonly listItemSpacingPx = SubscribableUtils.toSubscribable(this.props.listItemSpacingPx ?? 0, true); + protected readonly listItemLengthPxProp = SubscribableUtils.toSubscribable(this.props.listItemLengthPx, true); + protected readonly listItemSpacingPxProp = SubscribableUtils.toSubscribable(this.props.listItemSpacingPx ?? 0, true); + protected readonly itemsPerPageProp = this.props.itemsPerPage === undefined ? undefined : SubscribableUtils.toSubscribable(this.props.itemsPerPage, true); + + protected readonly listItemLengthPx = Subject.create(this.listItemLengthPxProp.get()); + protected readonly listItemSpacingPx = Subject.create(this.listItemSpacingPxProp.get()); protected readonly itemCount = SubscribableUtils.toSubscribable(this.props.itemCount, true); /** The axis along which this list scrolls. */ public readonly scrollAxis = this.props.scrollAxis ?? 'y'; + protected readonly _itemsPerPage = this.itemsPerPageProp === undefined ? undefined : Subject.create(this.itemsPerPageProp.get()); /** * The number of visible list items per page displayed by this list, or `undefined` if the number of items per page * is not prescribed. */ - public readonly itemsPerPage = this.props.itemsPerPage === undefined ? undefined : SubscribableUtils.toSubscribable(this.props.itemsPerPage, true); + public readonly itemsPerPage = this._itemsPerPage as Subscribable | undefined; protected readonly snapToItem = this.itemsPerPage !== undefined; @@ -245,10 +250,30 @@ export class ScrollList

extends Dis protected goToAnimationTargetPos = false; protected interval?: number; + protected readonly listItemParamSubs: Subscription[] = []; + protected cssClassSub?: Subscription | Subscription[]; /** @inheritdoc */ public onAfterRender(): void { + if (this._itemsPerPage && this.itemsPerPageProp) { + this.listItemLengthPx.set(this.listItemLengthPxProp.get()); + this.listItemSpacingPx.set(this.listItemSpacingPxProp.get()); + + this._itemsPerPage.set(this.itemsPerPageProp.get()); + + this.listItemParamSubs.push( + this.listItemLengthPxProp.sub(this.onListItemParamChanged.bind(this, this.listItemLengthPx)), + this.listItemSpacingPxProp.sub(this.onListItemParamChanged.bind(this, this.listItemSpacingPx)), + this.itemsPerPageProp.sub(this.onListItemParamChanged.bind(this, this._itemsPerPage)) + ); + } else { + this.listItemParamSubs.push( + this.listItemLengthPxProp.pipe(this.listItemLengthPx), + this.listItemSpacingPxProp.pipe(this.listItemSpacingPx), + ); + } + this._lengthPx.sub(lengthPx => { this.rootRef.instance.style.setProperty('--scroll-list-length', lengthPx + 'px'); }, true); @@ -626,8 +651,14 @@ export class ScrollList

extends Dis } } + const maxOverscrollPx = this.maxOverscrollPx.get(); + // Apply velocity to the scroll position - this._scrollPos.set(this._scrollPos.get() + (this.velocity * deltaTimeSeconds)); + this._scrollPos.set(MathUtils.clamp( + this._scrollPos.get() + (this.velocity * deltaTimeSeconds), + -maxOverscrollPx, + this._maxScrollPos.get() + maxOverscrollPx + )); // If we have scrolled past our target, set scroll position to the target and stop if (direction > 0) { @@ -653,6 +684,23 @@ export class ScrollList

extends Dis return MathUtils.clamp(MathUtils.round(pos, this._listItemLengthWithMarginPx.get()), 0, this._maxScrollPos.get()); } + /** + * Responds to when one of this list's item parameters changes when the list supports snapping to items. + * @param pipeTo The mutable subscribable to which to pipe the new parameter value. + * @param value The new parameter value. + */ + protected onListItemParamChanged(pipeTo: MutableSubscribable, value: number): void { + // If a list item parameter changes, then after the change the list's scroll position may be misaligned with + // respect to item snapping. Therefore, we will store the first visible item in the list before the parameter + // change and force the list to snap to the stored item after the parameter change. + + const firstVisibleIndex = this._firstVisibleIndex.get(); + + pipeTo.set(value); + + this.scrollToIndex(firstVisibleIndex, 0, false); + } + /** * Updates this list's item render window. */ @@ -730,14 +778,31 @@ export class ScrollList

extends Dis * @returns a number used to dampen the mouse movement when overscrolled. */ protected getDampening(direction: number): number { - const overscrollDirection = this.scrollPosFraction.get() > 1 ? 1 : this.scrollPosFraction.get() < 0 ? -1 : 0; - const overscrollPercentage = Math.min(1, this.getOverscrollPx() / this.maxOverscrollPx.get()); - const overscrollDampening = overscrollPercentage > 0 ? 0.5 : 1; + const maxScrollPos = this.maxScrollPos.get(); + const maxOverscrollPx = this.maxOverscrollPx.get(); + + // If we can't scroll at all, always dampen velocity to zero. + if (maxScrollPos <= 0 && maxOverscrollPx <= 0) { + return 0; + } + + const scrollPosFraction = this.scrollPosFraction.get(); + const overscrollDirection = scrollPosFraction >= 1 ? 1 : scrollPosFraction <= 0 ? -1 : 0; + + // If we are not trying to increase overscroll, then do not dampen velocity. if (overscrollDirection !== direction) { - return overscrollDampening; + return 1; + } + + if (maxOverscrollPx > 0) { + // If we can overscroll, then dampen velocity to zero if we are at the overscroll limit or by half if we are not + // at the limit. + const overscrollPercentage = Math.min(1, this.getOverscrollPx() / maxOverscrollPx); + return overscrollPercentage === 1 ? 0 : 0.5; + } else { + // If we can't overscroll, then always dampen velocity to zero. + return 0; } - const dampening = overscrollPercentage === 1 ? 0 : overscrollDampening; - return dampening; } /** @@ -801,6 +866,10 @@ export class ScrollList

extends Dis /** @inheritdoc */ public destroy(): void { + for (const sub of this.listItemParamSubs) { + sub.destroy(); + } + this._listItemLengthWithMarginPx.destroy(); 'destroy' in this._lengthPx && this._lengthPx.destroy(); this.pageLength.destroy(); diff --git a/src/garminsdk/components/map/GarminMapBuilder.tsx b/src/garminsdk/components/map/GarminMapBuilder.tsx index 95a8bc3e6..fa701d34b 100644 --- a/src/garminsdk/components/map/GarminMapBuilder.tsx +++ b/src/garminsdk/components/map/GarminMapBuilder.tsx @@ -17,18 +17,20 @@ import { TrafficSystem } from '../../traffic/TrafficSystem'; import { WindDataProvider } from '../../wind/WindDataProvider'; import { MapAirspaceVisController, MapAirspaceVisControllerModules, MapAirspaceVisUserSettings, MapDataIntegrityRTRController, MapDataIntegrityRTRControllerContext, - MapDataIntegrityRTRControllerModules, MapFlightPlanFocusRTRController, MapFlightPlanFocusRTRControllerContext, MapFlightPlanFocusRTRControllerModules, + MapDataIntegrityRTRControllerModules, MapDesiredOrientationController, MapDesiredOrientationControllerContext, MapDesiredOrientationControllerModules, + MapFlightPlanFocusRTRController, MapFlightPlanFocusRTRControllerContext, MapFlightPlanFocusRTRControllerModules, MapGarminAutopilotPropsBinding, MapGarminAutopilotPropsController, MapGarminAutopilotPropsControllerModules, MapGarminAutopilotPropsKey, MapGarminTrafficController, MapGarminTrafficControllerModules, MapNexradController, MapNexradControllerModules, MapNexradUserSettings, - MapOrientationController, MapOrientationControllerContext, MapOrientationControllerModules, MapOrientationControllerSettings, MapOrientationRTRController, - MapOrientationRTRControllerContext, MapOrientationRTRControllerModules, MapPointerController, MapPointerControllerModules, MapPointerRTRController, - MapPointerRTRControllerContext, MapPointerRTRControllerModules, MapRangeCompassController, MapRangeCompassControllerModules, MapRangeController, - MapRangeControllerModules, MapRangeControllerSettings, MapRangeRTRController, MapRangeRTRControllerModules, MapTerrainColorsController, - MapTerrainColorsControllerModules, MapTerrainColorsDefinition, MapTerrainController, MapTerrainControllerModules, MapTerrainUserSettings, + MapOrientationModeController, MapOrientationModeControllerContext, MapOrientationModeControllerModules, MapOrientationRTRController, + MapOrientationRTRControllerContext, MapOrientationRTRControllerModules, MapOrientationSettingsController, MapOrientationSettingsControllerModules, + MapOrientationSettingsControllerSettings, MapPanningRTRController, MapPanningRTRControllerContext, MapPanningRTRControllerModules, MapPointerController, + MapPointerControllerModules, MapPointerRTRController, MapPointerRTRControllerModules, MapRangeCompassController, MapRangeCompassControllerModules, + MapRangeController, MapRangeControllerModules, MapRangeControllerSettings, MapRangeRTRController, MapRangeRTRControllerModules, MapTerrainColorsController, + MapTerrainColorsControllerModules, MapTerrainColorsDefinition, MapTerrainController, MapTerrainControllerModules, MapTerrainControllerOptions, MapTerrainUserSettings, MapTrafficController, MapTrafficControllerModules, MapTrafficUserSettings, MapWaypointsVisController, MapWaypointsVisControllerModules, - MapWaypointVisUserSettings, MapWindVectorController, MapWindVectorControllerModules, MapWindVectorUserSettings, MapWxrController, MapWxrControllerModules, - TrafficMapRangeController, TrafficMapRangeControllerModules, TrafficMapRangeControllerSettings, WeatherMapOrientationController, - WeatherMapOrientationControllerContext, WeatherMapOrientationControllerModules, WeatherMapOrientationControllerSettings + MapWaypointsVisControllerOptions, MapWaypointVisUserSettings, MapWindVectorController, MapWindVectorControllerModules, MapWindVectorUserSettings, + MapWxrController, MapWxrControllerModules, TrafficMapRangeController, TrafficMapRangeControllerModules, TrafficMapRangeControllerSettings, + WeatherMapOrientationSettingsController, WeatherMapOrientationSettingsControllerModules, WeatherMapOrientationSettingsControllerSettings } from './controllers'; import { MapActiveFlightPlanDataProvider, MapFlightPathPlanRenderer, MapFlightPathProcRenderer, MapFlightPlannerPlanDataProvider } from './flightplan'; import { MapFlightPlanDataProvider } from './flightplan/MapFlightPlanDataProvider'; @@ -48,7 +50,7 @@ import { MapWaypointDisplayBuilder, MapWaypointDisplayBuilderClass } from './Map import { MapWaypointRenderer, MapWaypointRenderRole } from './MapWaypointRenderer'; import { GarminAirspaceShowTypeMap, MapCrosshairModule, MapDeclutterMode, MapDeclutterModule, MapFlightPlanFocusModule, MapGarminAutopilotPropsModule, - MapGarminDataIntegrityModule, MapGarminTrafficModule, MapNexradModule, MapOrientation, MapOrientationModule, MapPointerModule, MapProcedurePreviewModule, + MapGarminDataIntegrityModule, MapGarminTrafficModule, MapNexradModule, MapOrientation, MapOrientationModule, MapPanningModule, MapPointerModule, MapProcedurePreviewModule, MapRangeCompassModule, MapRangeRingModule, MapTerrainMode, MapTerrainModule, MapTrackVectorModule, MapUnitsModule, MapWaypointHighlightModule, MapWaypointsModule, MapWindVectorModule } from './modules'; @@ -126,7 +128,7 @@ export type TrackVectorOptions = Omit, keyof MapLayerProps>; /** - * + * A builder for Garmin maps. */ export class GarminMapBuilder { @@ -282,19 +284,21 @@ export class GarminMapBuilder { * Adds the following... * * Context properties: - * * `'[MapSystemKeys.RotationControl]': ResourceModerator` + * * `[MapSystemKeys.RotationControl]: ResourceModerator` * * `[GarminMapKeys.OrientationControl]: ResourceModerator` + * * `[GarminMapKeys.DesiredOrientationControl]: ResourceModerator` * * Modules: * * `[MapSystemKeys.Rotation]: MapRotationModule` * * `[GarminMapKeys.Orientation]: MapOrientationModule` - * * `[GarminMapKeys.Range]: MapIndexedRangeModule` (only with user setting support) + * * `[GarminMapKeys.Range]: MapIndexedRangeModule` + * * `[MapSystemKeys.OwnAirplaneProps]: MapOwnAirplanePropsModule` * * Controllers: * * `[MapSystemKeys.Rotation]: MapRotationController` * * `[GarminMapKeys.OrientationRTR]: MapOrientationRTRController` - * * `[GarminMapKeys.Orientation]: MapOrientationController` (only with user setting support) - * {@link MapOrientationRTRController}. + * * `[GarminMapKeys.Orientation]: MapOrientationModeController` + * * `[GarminMapKeys.DesiredOrientation]: MapDesiredOrientationController` * @param mapBuilder The map builder to configure. * @param nominalTargetOffsets The nominal projected target offsets defined by each orientation. Each target offset * is a 2-tuple `[x, y]`, where each component is expressed relative to the width or height of the map's projected @@ -303,20 +307,18 @@ export class GarminMapBuilder { * is a 4-tuple `[x1, y1, x2, y2]`, where each component is expressed relative to the width or height of the map's * projected window, *excluding* the dead zone. If an orientation does not have defined range endpoints, it will * default to `[0.5, 0.5, 0.5, 0]` (center to top-center). - * @param settingManager A setting manager containing user settings used to control the map orientation. If not - * defined, map orientation will not be bound to user settings. * @returns The map builder, after it has been configured. */ - public static orientation( + private static orientationBase( mapBuilder: MapBuilder, nominalTargetOffsets?: Partial>>, - nominalRangeEndpoints?: Partial>>, - settingManager?: UserSettingManager> + nominalRangeEndpoints?: Partial>> ): MapBuilder { mapBuilder .withRotation() .withContext(GarminMapKeys.RotationModeControl, () => new ResourceModerator(undefined)) .withContext(GarminMapKeys.OrientationControl, () => new ResourceModerator(undefined)) + .withContext(GarminMapKeys.DesiredOrientationControl, () => new ResourceModerator(undefined)) .withModule(GarminMapKeys.Orientation, () => new MapOrientationModule()) .withController( GarminMapKeys.OrientationRTR, @@ -325,15 +327,69 @@ export class GarminMapBuilder { nominalTargetOffsets, nominalRangeEndpoints ) + ) + .withController( + GarminMapKeys.Orientation, + context => new MapOrientationModeController(context) + ) + .withModule(GarminMapKeys.Range, () => new MapIndexedRangeModule()) + .withModule(MapSystemKeys.OwnAirplaneProps, () => new MapOwnAirplanePropsModule()) + .withController( + GarminMapKeys.DesiredOrientation, + context => new MapDesiredOrientationController(context) ); + return mapBuilder; + } + + /** + * Configures a map builder to generate a map which supports different orientations, as enumerated by + * {@link MapOrientation}. Each orientation defines a different rotation behavior, target offset, and range + * endpoints. + * + * Adds the following... + * + * Context properties: + * * `[MapSystemKeys.RotationControl]: ResourceModerator` + * * `[GarminMapKeys.OrientationControl]: ResourceModerator` + * * `[GarminMapKeys.DesiredOrientationControl]: ResourceModerator` + * + * Modules: + * * `[MapSystemKeys.Rotation]: MapRotationModule` + * * `[GarminMapKeys.Orientation]: MapOrientationModule` + * * `[GarminMapKeys.Range]: MapIndexedRangeModule` + * + * Controllers: + * * `[MapSystemKeys.Rotation]: MapRotationController` + * * `[GarminMapKeys.OrientationRTR]: MapOrientationRTRController` + * * `[GarminMapKeys.Orientation]: MapOrientationModeController` + * * `[GarminMapKeys.DesiredOrientation]: MapDesiredOrientationController` + * * `[GarminMapKeys.OrientationSettings]: MapOrientationSettingsController` (only with user setting support) + * @param mapBuilder The map builder to configure. + * @param nominalTargetOffsets The nominal projected target offsets defined by each orientation. Each target offset + * is a 2-tuple `[x, y]`, where each component is expressed relative to the width or height of the map's projected + * window, *excluding* the dead zone. If an orientation does not have a defined offset, it will default to `[0, 0]`. + * @param nominalRangeEndpoints The nominal range endpoints defined by each orientation. Each set of range endpoints + * is a 4-tuple `[x1, y1, x2, y2]`, where each component is expressed relative to the width or height of the map's + * projected window, *excluding* the dead zone. If an orientation does not have defined range endpoints, it will + * default to `[0.5, 0.5, 0.5, 0]` (center to top-center). + * @param settingManager A setting manager containing user settings used to control the map orientation. If not + * defined, map orientation will not be bound to user settings. + * @returns The map builder, after it has been configured. + */ + public static orientation( + mapBuilder: MapBuilder, + nominalTargetOffsets?: Partial>>, + nominalRangeEndpoints?: Partial>>, + settingManager?: UserSettingManager> + ): MapBuilder { + mapBuilder.with(GarminMapBuilder.orientationBase, nominalTargetOffsets, nominalRangeEndpoints); + if (settingManager !== undefined) { - mapBuilder - .withModule(GarminMapKeys.Range, () => new MapIndexedRangeModule()) - .withController( - GarminMapKeys.Orientation, - context => new MapOrientationController(context, settingManager) - ); + mapBuilder.withController( + GarminMapKeys.OrientationSettings, + context => new MapOrientationSettingsController(context, settingManager) + ); } return mapBuilder; @@ -347,19 +403,21 @@ export class GarminMapBuilder { * Adds the following... * * Context properties: - * * `'[MapSystemKeys.RotationControl]': ResourceModerator` + * * `[MapSystemKeys.RotationControl]: ResourceModerator` * * `[GarminMapKeys.OrientationControl]: ResourceModerator` + * * `[GarminMapKeys.DesiredOrientationControl]: ResourceModerator` * * Modules: * * `[MapSystemKeys.Rotation]: MapRotationModule` * * `[GarminMapKeys.Orientation]: MapOrientationModule` - * * `[GarminMapKeys.Range]: MapIndexedRangeModule` (only with user setting support) + * * `[GarminMapKeys.Range]: MapIndexedRangeModule` * * Controllers: * * `[MapSystemKeys.Rotation]: MapRotationController` * * `[GarminMapKeys.OrientationRTR]: MapOrientationRTRController` - * * `[GarminMapKeys.Orientation]: MapOrientationController` (only with user setting support) - * {@link MapOrientationRTRController}. + * * `[GarminMapKeys.Orientation]: MapOrientationModeController` + * * `[GarminMapKeys.DesiredOrientation]: MapDesiredOrientationController` + * * `[GarminMapKeys.OrientationSettings]: WeatherMapOrientationSettingsController` (only with user setting support) * @param mapBuilder The map builder to configure. * @param nominalTargetOffsets The nominal projected target offsets defined by each orientation. Each target offset * is a 2-tuple `[x, y]`, where each component is expressed relative to the width or height of the map's projected @@ -376,29 +434,15 @@ export class GarminMapBuilder { mapBuilder: MapBuilder, nominalTargetOffsets?: Partial>>, nominalRangeEndpoints?: Partial>>, - settingManager?: UserSettingManager> + settingManager?: UserSettingManager> ): MapBuilder { - mapBuilder - .withRotation() - .withContext(GarminMapKeys.RotationModeControl, () => new ResourceModerator(undefined)) - .withContext(GarminMapKeys.OrientationControl, () => new ResourceModerator(undefined)) - .withModule(GarminMapKeys.Orientation, () => new MapOrientationModule()) - .withController( - GarminMapKeys.OrientationRTR, - context => new MapOrientationRTRController( - context, - nominalTargetOffsets, - nominalRangeEndpoints - ) - ); + mapBuilder.with(GarminMapBuilder.orientationBase, nominalTargetOffsets, nominalRangeEndpoints); if (settingManager !== undefined) { - mapBuilder - .withModule(GarminMapKeys.Range, () => new MapIndexedRangeModule()) - .withController( - GarminMapKeys.Orientation, - context => new WeatherMapOrientationController(context, settingManager) - ); + mapBuilder.withController( + GarminMapKeys.OrientationSettings, + context => new WeatherMapOrientationSettingsController(context, settingManager) + ); } return mapBuilder; @@ -517,8 +561,9 @@ export class GarminMapBuilder { * @param colors The terrain colors to use for each terrain mode. Ignored if `includeTerrain` is `false`. * @param settingManager A user setting manager containing settings which control terrain colors. If not defined, * terrain color mode will not be controlled by user settings. - * @param allowRelativeMode Whether to allow relative terrain mode. Defaults to `true`. Ignored if terrain - * colors is not controlled by user settings. + * @param terrainModeOptions Options with which to configure the terrain mode controller. If a `boolean` value is + * provided in place of an options object, then it will be interpreted as the `allowRelative` option. Ignored if + * terrain colors is not controlled by user settings. * @param groundRelativeBlendDuration The amount of time, in milliseconds, over which to blend the on-ground and * relative terrain mode colors when transitioning between the two. A blend transition is only possible if colors * are defined for both the on-ground and relative terrain modes, and the colors for both modes have the same number @@ -529,7 +574,7 @@ export class GarminMapBuilder { mapBuilder: MapBuilder, colors: Partial>, settingManager?: UserSettingManager>, - allowRelativeMode = true, + terrainModeOptions?: Readonly | boolean, groundRelativeBlendDuration = 0 ): MapBuilder { mapBuilder @@ -543,7 +588,7 @@ export class GarminMapBuilder { mapBuilder .withModule(GarminMapKeys.Range, () => new MapIndexedRangeModule()) .withController(GarminMapKeys.Terrain, context => { - return new MapTerrainController(context, settingManager, allowRelativeMode); + return new MapTerrainController(context, settingManager, terrainModeOptions as any); }); } @@ -651,7 +696,7 @@ export class GarminMapBuilder { * Controllers: * * `[GarminMapKeys.RangeCompass]: MapRangeCompassController` * @param mapBuilder The map builder to configure. - * @param options Styling options for the ring. + * @param options Styling options for the compass. * @param order The order to assign to the range compass layer. Layers with lower assigned order will be attached to * the map before and appear below layers with greater assigned order values. Defaults to the number of layers * already added to the map builder. @@ -752,7 +797,7 @@ export class GarminMapBuilder { * Controllers: * * `[MapSystemKeys.WaypointRenderer]: MapSystemCustomController` (handles initialization and updating of the * waypoint renderer) - * * `'waypointsVis': MapWaypointsVisController` (only if user settings are supported) + * * `[GarminMapKeys.WaypointsVisibility]: MapWaypointsVisController` (only if user settings are supported) * @param mapBuilder The map builder to configure. * @param configure A function used to configure the display and styling of waypoint icons and labels. * @param supportRunwayOutlines Whether to support the rendering of airport runway outlines. @@ -765,7 +810,7 @@ export class GarminMapBuilder { */ public static waypoints( mapBuilder: MapBuilder, - configure: (builder: MapWaypointDisplayBuilder) => void, + configure: (builder: MapWaypointDisplayBuilder, context: MapSystemContext) => void, supportRunwayOutlines: boolean, settingManager?: UserSettingManager>, order?: number @@ -807,7 +852,7 @@ export class GarminMapBuilder { any, any, any, { [GarminMapKeys.WaypointDisplayBuilder]: MapWaypointDisplayBuilderClass } >('waypointsLayerDisplayConfigure', context => { - configure(context[GarminMapKeys.WaypointDisplayBuilder]); + configure(context[GarminMapKeys.WaypointDisplayBuilder], context); }) .withController< MapSystemGenericController, @@ -830,13 +875,15 @@ export class GarminMapBuilder { * Adds the controller `[GarminMapKeys.WaypointsVisibility]: MapWaypointsVisController`. * @param mapBuilder The map builder to configure. * @param settingManager A setting manager containing the user settings controlling waypoint visibility. + * @param options Options with which to configure waypoint visibility. * @returns The map builder, after it has been configured. */ public static waypointVisSettings>( mapBuilder: MapBuilder, - settingManager: UserSettingManager> + settingManager: UserSettingManager>, + options?: Readonly ): MapBuilder { - return mapBuilder.withController(GarminMapKeys.WaypointsVisibility, context => new MapWaypointsVisController(context, settingManager)); + return mapBuilder.withController(GarminMapKeys.WaypointsVisibility, context => new MapWaypointsVisController(context, settingManager, options)); } public static flightPlans( @@ -1513,19 +1560,21 @@ export class GarminMapBuilder { * Configures a map builder to generate a map with pointer support. Activating the pointer allows the pointer to * control map panning and stops the map from actively rotating. * - * If map target and rotation control resource moderators exist on the map context, the pointer RTR controller will - * attempt to claim those resources with a priority of `100`. Otherwise, the controller assumes nothing else controls - * the map target or rotation. + * If map target, orientation, or rotation control resource moderators exist on the map context, the panning RTR + * controller will attempt to claim those resources with a priority of `100`. Otherwise, the controller assumes + * nothing else controls the map target or rotation. * * Adds the following... * * Modules: + * * `[GarminMapKeys.Panning]: MapPanningModule` * * `[GarminMapKeys.Pointer]: MapPointerModule` * * Layers: * * `[GarminMapKeys.Pointer]: MapPointerLayer` * * Controllers: + * * `[GarminMapKeys.PanningRTR]: MapPanningRTRController` * * `[GarminMapKeys.Pointer]: MapPointerController` (can be used to control the behavior of the pointer) * * `[GarminMapKeys.PointerRTR]: MapPointerRTRController` * @param mapBuilder The map builder to configure. @@ -1539,13 +1588,14 @@ export class GarminMapBuilder { * added to the map builder. * @returns The map builder, after it has been configured. */ - public static pointer>( + public static pointer>( mapBuilder: MapBuilder, pointerBoundsOffset: ReadonlyFloat64Array | Subscribable, icon?: VNode, order?: number ): MapBuilder { return mapBuilder + .withModule(GarminMapKeys.Panning, () => new MapPanningModule()) .withModule(GarminMapKeys.Pointer, () => new MapPointerModule()) .withLayer(GarminMapKeys.Pointer, (context): VNode => { return ( @@ -1554,14 +1604,15 @@ export class GarminMapBuilder { ); }, order) + .withController( + GarminMapKeys.PanningRTR, + context => new MapPanningRTRController(context) + ) .withController(GarminMapKeys.Pointer, context => new MapPointerController(context)) - .withController( + .withController( GarminMapKeys.PointerRTR, context => { - return new MapPointerRTRController( - context, - 'isSubscribable' in pointerBoundsOffset ? pointerBoundsOffset : Subject.create(pointerBoundsOffset) - ); + return new MapPointerRTRController(context, pointerBoundsOffset); } ); } diff --git a/src/garminsdk/components/map/GarminMapKeys.ts b/src/garminsdk/components/map/GarminMapKeys.ts index 981f03438..ebe1266bb 100644 --- a/src/garminsdk/components/map/GarminMapKeys.ts +++ b/src/garminsdk/components/map/GarminMapKeys.ts @@ -14,10 +14,16 @@ export class GarminMapKeys { public static readonly OrientationRTR = 'orientationRTR' as const; + public static readonly DesiredOrientation = 'desiredOrientation' as const; + + public static readonly OrientationSettings = 'orientationSettings' as const; + public static readonly RotationModeControl = 'rotationModeControlModerator' as const; public static readonly OrientationControl = 'orientationControlModerator' as const; + public static readonly DesiredOrientationControl = 'desiredOrientationControl' as const; + public static readonly Declutter = 'declutter' as const; public static readonly Terrain = 'terrain' as const; @@ -34,6 +40,10 @@ export class GarminMapKeys { public static readonly WaypointsVisibility = 'waypointsVis' as const; + public static readonly RunwayVisibility = 'runwayVisibility' as const; + + public static readonly RunwayLabelVisibility = 'runwayLabelVisibility' as const; + public static readonly WaypointHighlight = 'waypointHighlight' as const; public static readonly WaypointHighlightLine = 'waypointHighlightLine' as const; @@ -50,6 +60,10 @@ export class GarminMapKeys { public static readonly TrafficRange = 'trafficRange' as const; + public static readonly Panning = 'panning' as const; + + public static readonly PanningRTR = 'panningRTR' as const; + public static readonly Pointer = 'pointer' as const; public static readonly PointerRTR = 'pointerRTR' as const; diff --git a/src/garminsdk/components/map/MapRangeDisplay.tsx b/src/garminsdk/components/map/MapRangeDisplay.tsx index 90ca091b7..50bf404eb 100644 --- a/src/garminsdk/components/map/MapRangeDisplay.tsx +++ b/src/garminsdk/components/map/MapRangeDisplay.tsx @@ -19,7 +19,8 @@ export interface MapRangeDisplayProps extends ComponentProps { } /** - * The map layer showing the range display. + * A display which renders a map range value with units. Automatically switches between nautical miles/feet and + * kilometers/meters at predefined thresholds. */ export class MapRangeDisplay extends DisplayComponent { private readonly displayUnitSub = Subject.create | null>(null); diff --git a/src/garminsdk/components/map/MapResourcePriority.ts b/src/garminsdk/components/map/MapResourcePriority.ts index 4f99f0456..b6dcc31a8 100644 --- a/src/garminsdk/components/map/MapResourcePriority.ts +++ b/src/garminsdk/components/map/MapResourcePriority.ts @@ -8,9 +8,15 @@ export class MapResourcePriority { /** Orientation mode. */ public static readonly ORIENTATION = 0; + /** Desired orientation mode. */ + public static readonly DESIRED_ORIENTATION = 0; + /** Rotation behavior from orientation mode. */ public static readonly ORIENTATION_ROTATION = 0; + /** Panning. */ + public static readonly PANNING = 100; + /** Pointer. */ public static readonly POINTER = 100; diff --git a/src/garminsdk/components/map/MapWaypointIcon.ts b/src/garminsdk/components/map/MapWaypointIcon.ts index ad1e8e9a7..b313d28be 100644 --- a/src/garminsdk/components/map/MapWaypointIcon.ts +++ b/src/garminsdk/components/map/MapWaypointIcon.ts @@ -45,22 +45,22 @@ export class MapAirportIcon extends MapWaypointSprite * Initialization options for a MapWaypointHighlightIcon. */ export type MapWaypointHighlightIconOptions = { - /** The buffer of the ring around the base icon, in pixels. */ + /** The buffer of the ring around the base icon, in pixels. Defaults to `0`. */ ringRadiusBuffer?: number | Subscribable; - /** The width of the stroke for the ring, in pixels. */ + /** The width of the stroke for the ring, in pixels. Defaults to `2`. */ strokeWidth?: number | Subscribable; - /** The color of the stroke for the ring. */ + /** The color of the stroke for the ring. Defaults to `'white'`. */ strokeColor?: string | Subscribable; - /** The width of the outline for the ring, in pixels. */ + /** The width of the outline for the ring, in pixels. Defaults to `0`. */ outlineWidth?: number | Subscribable; - /** The color of the outline for the ring. */ + /** The color of the outline for the ring. Defaults to `'black'`. */ outlineColor?: string | Subscribable; - /** The color of the ring background. */ + /** The color of the ring background. Defaults to `'#3c3c3c'`. */ bgColor?: string | Subscribable; } @@ -144,7 +144,9 @@ export class MapWaypointHighlightIcon extends AbstractMapWay if (outlineWidth > 0) { this.applyStroke(context, (strokeWidth + 2 * outlineWidth), this.outlineColor.get()); } - this.applyStroke(context, strokeWidth, this.strokeColor.get()); + if (strokeWidth > 0) { + this.applyStroke(context, strokeWidth, this.strokeColor.get()); + } } /** diff --git a/src/garminsdk/components/map/NextGenGarminMapBuilder.tsx b/src/garminsdk/components/map/NextGenGarminMapBuilder.tsx new file mode 100644 index 000000000..e9502aae0 --- /dev/null +++ b/src/garminsdk/components/map/NextGenGarminMapBuilder.tsx @@ -0,0 +1,97 @@ +/* eslint-disable jsdoc/require-jsdoc */ + +import { MapSystemBuilder, MapSystemContext, MapSystemKeys, Subject, UserSettingManager } from '@microsoft/msfs-sdk'; + +import { MapSymbolVisController, MapSymbolVisControllerModules, MapWaypointVisUserSettings } from './controllers'; +import { GarminMapBuilder } from './GarminMapBuilder'; +import { GarminMapKeys } from './GarminMapKeys'; +import { MapWaypointDisplayBuilder } from './MapWaypointDisplayBuilder'; +import { MapDeclutterMode, MapWaypointsModule } from './modules'; + +/** + * A builder for next-generation (NXi, G3000, etc) Garmin maps. + */ +export class NextGenGarminMapBuilder { + + /** + * Configures a map builder to generate a map which supports the display of waypoints located within the boundaries + * of the map's projected window. Waypoints displayed in this manner are rendered by a {@link MapWaypointRenderer} + * under the role {@link MapWaypointRenderRole.Normal}. Optionally binds the visibility of waypoints to user + * settings. + * + * If a text layer has already been added to the builder, its order will be changed so that it is rendered above the + * waypoint layer. Otherwise, a text layer will be added to the builder after the waypoint layer. + * + * Adds the following... + * + * Context properties: + * * `[MapSystemKeys.TextManager]: MapCullableTextLabelManager` + * * `[MapSystemKeys.WaypointRenderer]: MapWaypointRenderer` + * * `[GarminMapKeys.WaypointDisplayBuilder]: MapWaypointDisplayBuilder` + * + * Modules: + * * `[MapSystemKeys.NearestWaypoints]: MapWaypointsModule` + * * `[GarminMapKeys.Range]: MapIndexedRangeModule` (only if user settings are supported) + * + * Layers: + * * `[MapSystemKeys.NearestWaypoints]: MapWaypointsLayer` + * * `[MapSystemKeys.TextLayer]: MapCullableTextLayer` + * + * Controllers: + * * `[MapSystemKeys.WaypointRenderer]: MapSystemCustomController` (handles initialization and updating of the + * waypoint renderer) + * * `[GarminMapKeys.WaypointsVisibility]: MapWaypointsVisController` (only if user settings are supported) + * * `[GarminMapKeys.RunwayVisibility]: MapSymbolVisController` (only if runway outlines are supported) + * * `[GarminMapKeys.RunwayLabelVisibility]: MapSymbolVisController` (only if runway outlines are supported) + * @param mapBuilder The map builder to configure. + * @param configure A function used to configure the display and styling of waypoint icons and labels. + * @param supportRunwayOutlines Whether to support the rendering of airport runway outlines. + * @param settingManager A setting manager containing the user settings controlling waypoint visibility. If not + * defined, waypoint visibility will not be bound to user settings. + * @param order The order to assign to the waypoint layer. Layers with lower assigned order will be attached to the + * map before and appear below layers with greater assigned order values. Defaults to the number of layers already + * added to the map builder. + * @returns The map builder, after it has been configured. + */ + public static waypoints( + mapBuilder: MapBuilder, + configure: (builder: MapWaypointDisplayBuilder, context: MapSystemContext) => void, + supportRunwayOutlines: boolean, + settingManager?: UserSettingManager>, + order?: number + ): MapBuilder { + mapBuilder + .with(GarminMapBuilder.waypoints, configure, supportRunwayOutlines, settingManager, order); + + if (supportRunwayOutlines && settingManager) { + const trueSubject = Subject.create(true); + const maxSafeIntegerSubject = Subject.create(Number.MAX_SAFE_INTEGER); + + mapBuilder + .withController< + MapSymbolVisController, MapSymbolVisControllerModules & { [MapSystemKeys.NearestWaypoints]: MapWaypointsModule } + >(GarminMapKeys.RunwayVisibility, context => { + return new MapSymbolVisController( + context, + trueSubject, + maxSafeIntegerSubject, + MapDeclutterMode.Level2, + context.model.getModule(MapSystemKeys.NearestWaypoints).runwayShow + ); + }) + .withController< + MapSymbolVisController, MapSymbolVisControllerModules & { [MapSystemKeys.NearestWaypoints]: MapWaypointsModule } + >(GarminMapKeys.RunwayLabelVisibility, context => { + return new MapSymbolVisController( + context, + trueSubject, + maxSafeIntegerSubject, + MapDeclutterMode.Level2, + context.model.getModule(MapSystemKeys.NearestWaypoints).runwayLabelShow + ); + }); + } + + return mapBuilder; + } +} \ No newline at end of file diff --git a/src/garminsdk/components/map/assembled/NextGenConnextMapBuilder.tsx b/src/garminsdk/components/map/assembled/NextGenConnextMapBuilder.tsx index 763e872fb..02634043f 100644 --- a/src/garminsdk/components/map/assembled/NextGenConnextMapBuilder.tsx +++ b/src/garminsdk/components/map/assembled/NextGenConnextMapBuilder.tsx @@ -26,6 +26,7 @@ import { NextGenMapWaypointStyles } from '../MapWaypointStyles'; import { MapFlightPlanFocusModule, MapOrientation, MapOrientationModule, MapPointerModule, MapUnitsModule } from '../modules'; +import { NextGenGarminMapBuilder } from '../NextGenGarminMapBuilder'; /** * Options for creating a next-generation (NXi, G3000, etc) Garmin Connext weather map. @@ -323,7 +324,7 @@ export class NextGenConnextMapBuilder { mapBuilder.with(GarminMapBuilder.airspaces, options.useAirspaceVisUserSettings ? options.settingManager : undefined); } - mapBuilder.with(GarminMapBuilder.waypoints, + mapBuilder.with(NextGenGarminMapBuilder.waypoints, (builder: MapWaypointDisplayBuilder): void => { builder.withNormalStyles( options.waypointIconImageCache, @@ -390,16 +391,15 @@ export class NextGenConnextMapBuilder { } if (options.includeAltitudeArc) { - mapBuilder - .with(GarminMapBuilder.altitudeArc, - { - renderMethod: 'svg', - verticalSpeedPrecision: UnitType.FPM.createNumber(50), - verticalSpeedThreshold: UnitType.FPM.createNumber(150), - altitudeDeviationThreshold: UnitType.FOOT.createNumber(150) - }, - options.useAltitudeArcUserSettings ? options.settingManager : undefined - ); + mapBuilder.with(GarminMapBuilder.altitudeArc, + { + renderMethod: 'svg', + verticalSpeedPrecision: UnitType.FPM.createNumber(50), + verticalSpeedThreshold: UnitType.FPM.createNumber(150), + altitudeDeviationThreshold: UnitType.FOOT.createNumber(150) + }, + options.useAltitudeArcUserSettings ? options.settingManager : undefined + ); } mapBuilder.with(GarminMapBuilder.crosshair); diff --git a/src/garminsdk/components/map/assembled/NextGenHsiMapBuilder.tsx b/src/garminsdk/components/map/assembled/NextGenHsiMapBuilder.tsx index 45657b724..7401f62b8 100644 --- a/src/garminsdk/components/map/assembled/NextGenHsiMapBuilder.tsx +++ b/src/garminsdk/components/map/assembled/NextGenHsiMapBuilder.tsx @@ -22,6 +22,7 @@ import { MapUtils } from '../MapUtils'; import { MapWaypointDisplayBuilder } from '../MapWaypointDisplayBuilder'; import { NextGenMapWaypointStyles } from '../MapWaypointStyles'; import { MapDeclutterMode, MapDeclutterModule, MapGarminTrafficModule, MapOrientation, MapOrientationModule, MapTerrainMode, MapUnitsModule } from '../modules'; +import { NextGenGarminMapBuilder } from '../NextGenGarminMapBuilder'; /** * Configurations for traffic intruder icons for next-generation (NXi, G3000, etc) HSI maps. @@ -334,7 +335,7 @@ export class NextGenHsiMapBuilder { mapBuilder.with(GarminMapBuilder.airspaces, options.useAirspaceVisUserSettings ? options.settingManager : undefined); } - mapBuilder.with(GarminMapBuilder.waypoints, + mapBuilder.with(NextGenGarminMapBuilder.waypoints, (builder: MapWaypointDisplayBuilder): void => { builder.withNormalStyles( options.waypointIconImageCache, diff --git a/src/garminsdk/components/map/assembled/NextGenNavMapBuilder.tsx b/src/garminsdk/components/map/assembled/NextGenNavMapBuilder.tsx index 6d6ed16e4..9e486b91e 100644 --- a/src/garminsdk/components/map/assembled/NextGenNavMapBuilder.tsx +++ b/src/garminsdk/components/map/assembled/NextGenNavMapBuilder.tsx @@ -30,6 +30,7 @@ import { MapDeclutterMode, MapDeclutterModule, MapFlightPlanFocusModule, MapGarminTrafficModule, MapOrientation, MapOrientationModule, MapPointerModule, MapTerrainMode, MapTerrainModule, MapUnitsModule } from '../modules'; +import { NextGenGarminMapBuilder } from '../NextGenGarminMapBuilder'; /** * Configurations for traffic intruder icons for next-generation (NXi, G3000, etc) navigation maps. @@ -449,7 +450,7 @@ export class NextGenNavMapBuilder { mapBuilder.with(GarminMapBuilder.airspaces, options.useAirspaceVisUserSettings ? options.settingManager : undefined); } - mapBuilder.with(GarminMapBuilder.waypoints, + mapBuilder.with(NextGenGarminMapBuilder.waypoints, (builder: MapWaypointDisplayBuilder): void => { builder.withNormalStyles( options.waypointIconImageCache, @@ -521,20 +522,15 @@ export class NextGenNavMapBuilder { } if (options.includeAltitudeArc) { - mapBuilder - .withAutopilotProps( - ['selectedAltitude'], - options.dataUpdateFreq - ) - .with(GarminMapBuilder.altitudeArc, - { - renderMethod: 'svg', - verticalSpeedPrecision: UnitType.FPM.createNumber(50), - verticalSpeedThreshold: UnitType.FPM.createNumber(150), - altitudeDeviationThreshold: UnitType.FOOT.createNumber(150) - }, - options.useAltitudeArcUserSettings ? options.settingManager : undefined - ); + mapBuilder.with(GarminMapBuilder.altitudeArc, + { + renderMethod: 'svg', + verticalSpeedPrecision: UnitType.FPM.createNumber(50), + verticalSpeedThreshold: UnitType.FPM.createNumber(150), + altitudeDeviationThreshold: UnitType.FOOT.createNumber(150) + }, + options.useAltitudeArcUserSettings ? options.settingManager : undefined + ); } mapBuilder.with(GarminMapBuilder.crosshair); diff --git a/src/garminsdk/components/map/assembled/NextGenNearestMapBuilder.tsx b/src/garminsdk/components/map/assembled/NextGenNearestMapBuilder.tsx index ea2d10f94..7bd81c326 100644 --- a/src/garminsdk/components/map/assembled/NextGenNearestMapBuilder.tsx +++ b/src/garminsdk/components/map/assembled/NextGenNearestMapBuilder.tsx @@ -30,6 +30,7 @@ import { MapDeclutterMode, MapDeclutterModule, MapGarminTrafficModule, MapOrientation, MapOrientationModule, MapPointerModule, MapTerrainMode, MapTerrainModule, MapUnitsModule } from '../modules'; +import { NextGenGarminMapBuilder } from '../NextGenGarminMapBuilder'; /** * Configurations for traffic intruder icons for next-generation (NXi, G3000, etc) nearest waypoint maps. @@ -429,7 +430,7 @@ export class NextGenNearestMapBuilder { mapBuilder.with(GarminMapBuilder.airspaces, options.useAirspaceVisUserSettings ? options.settingManager : undefined); } - mapBuilder.with(GarminMapBuilder.waypoints, + mapBuilder.with(NextGenGarminMapBuilder.waypoints, (builder: MapWaypointDisplayBuilder): void => { builder.withNormalStyles( options.waypointIconImageCache, @@ -505,16 +506,15 @@ export class NextGenNearestMapBuilder { } if (options.includeAltitudeArc) { - mapBuilder - .with(GarminMapBuilder.altitudeArc, - { - renderMethod: 'svg', - verticalSpeedPrecision: UnitType.FPM.createNumber(50), - verticalSpeedThreshold: UnitType.FPM.createNumber(150), - altitudeDeviationThreshold: UnitType.FOOT.createNumber(150) - }, - options.useAltitudeArcUserSettings ? options.settingManager : undefined - ); + mapBuilder.with(GarminMapBuilder.altitudeArc, + { + renderMethod: 'svg', + verticalSpeedPrecision: UnitType.FPM.createNumber(50), + verticalSpeedThreshold: UnitType.FPM.createNumber(150), + altitudeDeviationThreshold: UnitType.FOOT.createNumber(150) + }, + options.useAltitudeArcUserSettings ? options.settingManager : undefined + ); } mapBuilder.with(GarminMapBuilder.crosshair); diff --git a/src/garminsdk/components/map/assembled/NextGenWaypointMapBuilder.tsx b/src/garminsdk/components/map/assembled/NextGenWaypointMapBuilder.tsx index d7836af4c..9597a3f9c 100644 --- a/src/garminsdk/components/map/assembled/NextGenWaypointMapBuilder.tsx +++ b/src/garminsdk/components/map/assembled/NextGenWaypointMapBuilder.tsx @@ -19,6 +19,7 @@ import { MapUtils } from '../MapUtils'; import { MapWaypointDisplayBuilder } from '../MapWaypointDisplayBuilder'; import { NextGenMapWaypointStyles } from '../MapWaypointStyles'; import { MapDeclutterMode, MapDeclutterModule, MapOrientation, MapOrientationModule, MapPointerModule, MapTerrainMode, MapUnitsModule, WaypointMapSelectionModule } from '../modules'; +import { NextGenGarminMapBuilder } from '../NextGenGarminMapBuilder'; /** * Options for creating a next-generation (NXi, G3000, etc) Garmin waypoint map. @@ -41,8 +42,7 @@ export type NextGenWaypointMapOptions = { /** * Whether the map should automatically adjust its range when the selected waypoint is an airport to give an - * appropriate view of the selected runway, or all runways if there is no selected runway. If `false`, the map will - * attempt to apply the range index defined by `defaultTargetRangeIndex` instead. Defaults to `false`. + * appropriate view of the selected runway, or all runways if there is no selected runway. Defaults to `false`. */ supportAirportAutoRange?: boolean; @@ -346,7 +346,7 @@ export class NextGenWaypointMapBuilder { mapBuilder.with(GarminMapBuilder.airspaces, options.useAirspaceVisUserSettings ? options.settingManager : undefined); } - mapBuilder.with(GarminMapBuilder.waypoints, + mapBuilder.with(NextGenGarminMapBuilder.waypoints, (builder: MapWaypointDisplayBuilder): void => { builder.withNormalStyles( options.waypointIconImageCache, diff --git a/src/garminsdk/components/map/controllers/MapDataIntegrityRTRController.ts b/src/garminsdk/components/map/controllers/MapDataIntegrityRTRController.ts index dd2b71640..27f3e1511 100644 --- a/src/garminsdk/components/map/controllers/MapDataIntegrityRTRController.ts +++ b/src/garminsdk/components/map/controllers/MapDataIntegrityRTRController.ts @@ -1,6 +1,6 @@ import { - MapDataIntegrityModule, MapOwnAirplaneIconModule, MapSystemContext, MapSystemController, MapSystemKeys, MutableSubscribable, ReadonlyFloat64Array, - ResourceConsumer, ResourceModerator, Subscribable, Subscription + MapDataIntegrityModule, MapOwnAirplaneIconModule, MapSystemContext, MapSystemController, MapSystemKeys, MappedSubject, + MappedSubscribable, MutableSubscribable, ReadonlyFloat64Array, ResourceConsumer, ResourceModerator, Subscribable, Subscription } from '@microsoft/msfs-sdk'; import { GarminMapKeys } from '../GarminMapKeys'; @@ -56,10 +56,14 @@ export class MapDataIntegrityRTRController extends MapSystemController { - this.orientationModule?.orientation.set(MapOrientation.NorthUp); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.orientationOverridePipe!.resume(true); }, - onCeded: () => { } + onCeded: () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.orientationOverridePipe!.pause(); + } }; private readonly canChangeAirplaneIcon @@ -70,6 +74,9 @@ export class MapDataIntegrityRTRController extends MapSystemController; + private orientationOverridePipe?: Subscription; + private headingSignalSub?: Subscription; private gpsSignalSub?: Subscription; @@ -122,17 +129,55 @@ export class MapDataIntegrityRTRController extends MapSystemController { + if (isHeadingValid && isGpsValid) { + return null; + } + + switch (desiredOrientation) { + case MapOrientation.HeadingUp: + return isHeadingValid ? desiredOrientation : MapOrientation.NorthUp; + case MapOrientation.TrackUp: + case MapOrientation.DtkUp: + return isGpsValid ? desiredOrientation : MapOrientation.NorthUp; + default: + return desiredOrientation; + } + }, + this.orientationModule.desiredOrientation, + this.dataIntegrityModule.headingSignalValid, + this.dataIntegrityModule.gpsSignalValid + ); + + this.orientationOverridePipe = this.orientationOverride.pipe(this.orientationModule.orientation, true); + + this.orientationOverride.sub(override => { + if (override === null) { + if (this.orientationControl === undefined) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.orientationOverridePipe!.pause(); + } else { + this.orientationControl.forfeit(this.orientationControlConsumer); + } + } else { + if (this.orientationControl === undefined) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.orientationOverridePipe!.resume(true); + } else { + this.orientationControl.claim(this.orientationControlConsumer); + } + } + }, true); + } + this.headingSignalSub = this.dataIntegrityModule.headingSignalValid.sub(isValid => { if (isValid) { this.orientationControl?.forfeit(this.orientationControlConsumer); this.setNormalAirplaneIcon(); } else { this.setNoHeadingAirplaneIcon(); - if (this.orientationControl === undefined) { - this.orientationModule?.orientation.set(MapOrientation.NorthUp); - } else { - this.orientationControl.claim(this.orientationControlConsumer); - } } }, true); @@ -176,11 +221,12 @@ export class MapDataIntegrityRTRController extends MapSystemController { + private readonly rangeModule = this.context.model.getModule(GarminMapKeys.Range); + private readonly orientationModule = this.context.model.getModule(GarminMapKeys.Orientation); + private readonly ownAirplanePropsModule = this.context.model.getModule(MapSystemKeys.OwnAirplaneProps); + + private readonly desiredOrientationControl = this.context[GarminMapKeys.DesiredOrientationControl]; + + private readonly desiredOrientationControlConsumer: ResourceConsumer = { + priority: MapResourcePriority.DESIRED_ORIENTATION, + + onAcquired: () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.orientationPipe!.resume(true); + }, + + onCeded: () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.orientationPipe!.pause(); + } + }; + + private orientation?: MappedSubscribable; + + private orientationPipe?: Subscription; + private isPointerActiveSub?: Subscription; + + /** @inheritdoc */ + public onAfterMapRender(): void { + this.orientation = MappedSubject.create( + ([commandedOrientation, isNorthUpAboveActive, northUpAboveRangeIndex, rangeIndex, isNorthUpOnGroundActive, isOnGround]): MapOrientation => { + return (isNorthUpAboveActive && rangeIndex > northUpAboveRangeIndex) || (isNorthUpOnGroundActive && isOnGround) + ? MapOrientation.NorthUp + : commandedOrientation; + }, + this.orientationModule.commandedOrientation, + this.orientationModule.northUpAboveActive, + this.orientationModule.northUpAboveRangeIndex, + this.rangeModule.nominalRangeIndex, + this.orientationModule.northUpOnGroundActive, + this.ownAirplanePropsModule.isOnGround + ); + + this.orientationPipe = this.orientation.pipe(this.orientationModule.desiredOrientation, true); + + this.desiredOrientationControl.claim(this.desiredOrientationControlConsumer); + } + + /** @inheritdoc */ + public onMapDestroyed(): void { + this.destroy(); + } + + /** @inheritdoc */ + public destroy(): void { + this.desiredOrientationControl.forfeit(this.desiredOrientationControlConsumer); + + this.orientation?.destroy(); + this.orientationPipe?.destroy(); + this.isPointerActiveSub?.destroy(); + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/garminsdk/components/map/controllers/MapFlightPlanFocusRTRController.ts b/src/garminsdk/components/map/controllers/MapFlightPlanFocusRTRController.ts index 180aedabc..ab5f26282 100644 --- a/src/garminsdk/components/map/controllers/MapFlightPlanFocusRTRController.ts +++ b/src/garminsdk/components/map/controllers/MapFlightPlanFocusRTRController.ts @@ -44,8 +44,8 @@ export interface MapFlightPlanFocusRTRControllerContext { /** Resource moderator for control of the map's projection target. */ [MapSystemKeys.TargetControl]?: ResourceModerator; - /** Resource moderator for control of the map's orientation mode. */ - [GarminMapKeys.OrientationControl]?: ResourceModerator; + /** Resource moderator for control of the map's desired orientation mode. */ + [GarminMapKeys.DesiredOrientationControl]?: ResourceModerator; /** Resource moderator for the use range setting subject. */ [GarminMapKeys.UseRangeSetting]?: ResourceModerator>; @@ -96,13 +96,13 @@ export class MapFlightPlanFocusRTRController extends MapSystemController< } }; - private readonly orientationControl = this.context[GarminMapKeys.OrientationControl]; + private readonly desiredOrientationControl = this.context[GarminMapKeys.DesiredOrientationControl]; - private readonly orientationControlConsumer: ResourceConsumer = { + private readonly desiredOrientationControlConsumer: ResourceConsumer = { priority: MapResourcePriority.FLIGHT_PLAN_FOCUS, onAcquired: () => { - this.orientationModule?.orientation.set(MapOrientation.NorthUp); + this.orientationModule?.desiredOrientation.set(MapOrientation.NorthUp); }, onCeded: () => { } @@ -232,16 +232,16 @@ export class MapFlightPlanFocusRTRController extends MapSystemController< */ private onIsFocusActiveChanged(isActive: boolean): void { if (isActive) { - if (this.orientationControl === undefined) { + if (this.desiredOrientationControl === undefined) { // If there is no moderator, assume we have control - this.orientationModule?.orientation.set(MapOrientation.NorthUp); + this.orientationModule?.desiredOrientation.set(MapOrientation.NorthUp); } else { - this.orientationControl.claim(this.orientationControlConsumer); + this.desiredOrientationControl.claim(this.desiredOrientationControlConsumer); } } else { this.focusDebounceTimer.clear(); - this.orientationControl?.forfeit(this.orientationControlConsumer); + this.desiredOrientationControl?.forfeit(this.desiredOrientationControlConsumer); } this.setFlightPlanFocusListenersActive(isActive); @@ -359,7 +359,7 @@ export class MapFlightPlanFocusRTRController extends MapSystemController< super.destroy(); this.targetControl?.forfeit(this.targetControlConsumer); - this.orientationControl?.forfeit(this.orientationControlConsumer); + this.desiredOrientationControl?.forfeit(this.desiredOrientationControlConsumer); this.useRangeSetting?.forfeit(this.useRangeSettingConsumer); this.isPlanFocusValid.destroy(); diff --git a/src/garminsdk/components/map/controllers/MapOrientationController.ts b/src/garminsdk/components/map/controllers/MapOrientationController.ts index 0b53fe55b..74231a825 100644 --- a/src/garminsdk/components/map/controllers/MapOrientationController.ts +++ b/src/garminsdk/components/map/controllers/MapOrientationController.ts @@ -11,6 +11,7 @@ import { MapOrientation, MapOrientationModule } from '../modules/MapOrientationM /** * Modules required for MapOrientationController. + * @deprecated */ export interface MapOrientationControllerModules { /** Range module. */ @@ -25,6 +26,7 @@ export interface MapOrientationControllerModules { /** * Context properties required by MapOrientationController. + * @deprecated */ export interface MapOrientationControllerContext { /** Resource moderator for control of the map's orientation mode. */ @@ -33,17 +35,21 @@ export interface MapOrientationControllerContext { /** * User settings required by MapOrientationController. + * @deprecated */ export type MapOrientationControllerSettings = Pick; /** * Controls the orientation of a map based on user settings. + * @deprecated New, preferred logic for controlling map orientation based on user settings is available using + * `MapOrientationSettingsController`. */ export class MapOrientationController extends MapSystemController { - private static readonly MODE_MAP = { + private static readonly MODE_MAP: Partial> = { [MapOrientationSettingMode.NorthUp]: MapOrientation.NorthUp, [MapOrientationSettingMode.HeadingUp]: MapOrientation.HeadingUp, - [MapOrientationSettingMode.TrackUp]: MapOrientation.TrackUp + [MapOrientationSettingMode.TrackUp]: MapOrientation.TrackUp, + [MapOrientationSettingMode.DtkUp]: MapOrientation.DtkUp }; private readonly rangeModule = this.context.model.getModule(GarminMapKeys.Range); diff --git a/src/garminsdk/components/map/controllers/MapOrientationModeController.ts b/src/garminsdk/components/map/controllers/MapOrientationModeController.ts new file mode 100644 index 000000000..a23cd0b76 --- /dev/null +++ b/src/garminsdk/components/map/controllers/MapOrientationModeController.ts @@ -0,0 +1,69 @@ +import { + MapSystemController, ResourceConsumer, ResourceModerator, Subscription +} from '@microsoft/msfs-sdk'; + +import { GarminMapKeys } from '../GarminMapKeys'; +import { MapOrientationModule } from '../modules/MapOrientationModule'; +import { MapResourcePriority } from '../MapResourcePriority'; + +/** + * Modules required for MapOrientationModeController. + */ +export interface MapOrientationModeControllerModules { + /** Orientation module. */ + [GarminMapKeys.Orientation]: MapOrientationModule; +} + +/** + * Context properties required by MapOrientationModeController. + */ +export interface MapOrientationModeControllerContext { + /** Resource moderator for control of the map's orientation mode. */ + [GarminMapKeys.OrientationControl]: ResourceModerator; +} + +/** + * Controls the orientation of a map based on the desired orientation mode. + */ +export class MapOrientationModeController extends MapSystemController { + private readonly orientationModule = this.context.model.getModule(GarminMapKeys.Orientation); + + private readonly orientationControl = this.context[GarminMapKeys.OrientationControl]; + + private readonly orientationControlConsumer: ResourceConsumer = { + priority: MapResourcePriority.ORIENTATION, + + onAcquired: () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.orientationPipe!.resume(true); + }, + + onCeded: () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.orientationPipe!.pause(); + } + }; + + private orientationPipe?: Subscription; + + /** @inheritdoc */ + public onAfterMapRender(): void { + this.orientationPipe = this.orientationModule.desiredOrientation.pipe(this.orientationModule.orientation, true); + + this.orientationControl.claim(this.orientationControlConsumer); + } + + /** @inheritdoc */ + public onMapDestroyed(): void { + this.destroy(); + } + + /** @inheritdoc */ + public destroy(): void { + this.orientationControl.forfeit(this.orientationControlConsumer); + + this.orientationPipe?.destroy(); + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/garminsdk/components/map/controllers/MapOrientationRTRController.ts b/src/garminsdk/components/map/controllers/MapOrientationRTRController.ts index 4c53cbda9..fdd2fc66e 100644 --- a/src/garminsdk/components/map/controllers/MapOrientationRTRController.ts +++ b/src/garminsdk/components/map/controllers/MapOrientationRTRController.ts @@ -75,9 +75,11 @@ export class MapOrientationRTRController extends MapSystemController; + +/** + * Controls the orientation of a map based on user settings. + */ +export class MapOrientationSettingsController extends MapSystemController { + private static readonly MODE_MAP: Partial> = { + [MapOrientationSettingMode.NorthUp]: MapOrientation.NorthUp, + [MapOrientationSettingMode.HeadingUp]: MapOrientation.HeadingUp, + [MapOrientationSettingMode.TrackUp]: MapOrientation.TrackUp, + [MapOrientationSettingMode.DtkUp]: MapOrientation.DtkUp + }; + + private readonly orientationModule = this.context.model.getModule(GarminMapKeys.Orientation); + + private readonly subs: Subscription[] = []; + + /** + * Creates a new instance of MapOrientationSettingsController. + * @param context This controller's map context. + * @param settingManager The setting manager used by this controller. + */ + public constructor( + context: MapSystemContext, + private readonly settingManager: UserSettingManager> + ) { + super(context); + } + + /** @inheritdoc */ + public onAfterMapRender(): void { + const orientationSetting = this.settingManager.tryGetSetting('mapOrientation'); + if (orientationSetting) { + this.subs.push(orientationSetting.pipe(this.orientationModule.commandedOrientation, setting => { + return MapOrientationSettingsController.MODE_MAP[setting] ?? MapOrientation.NorthUp; + })); + } + + const northUpAboveActiveSetting = this.settingManager.tryGetSetting('mapAutoNorthUpActive'); + if (northUpAboveActiveSetting) { + this.subs.push(northUpAboveActiveSetting.pipe(this.orientationModule.northUpAboveActive, setting => { + return setting === true; + })); + } + + const northUpAboveRangeIndexSetting = this.settingManager.tryGetSetting('mapAutoNorthUpRangeIndex'); + if (northUpAboveRangeIndexSetting) { + this.subs.push(northUpAboveRangeIndexSetting.pipe(this.orientationModule.northUpAboveRangeIndex, setting => { + return typeof setting === 'number' ? setting : Infinity; + })); + } + + const northUpOnGroundActiveSetting = this.settingManager.tryGetSetting('mapGroundNorthUpActive'); + if (northUpOnGroundActiveSetting) { + this.subs.push(northUpOnGroundActiveSetting.pipe(this.orientationModule.northUpOnGroundActive, setting => { + return setting === true; + })); + } + } + + /** @inheritdoc */ + public onMapDestroyed(): void { + this.destroy(); + } + + /** @inheritdoc */ + public destroy(): void { + for (const sub of this.subs) { + sub.destroy(); + } + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/garminsdk/components/map/controllers/MapPanningRTRController.ts b/src/garminsdk/components/map/controllers/MapPanningRTRController.ts new file mode 100644 index 000000000..edf4e7347 --- /dev/null +++ b/src/garminsdk/components/map/controllers/MapPanningRTRController.ts @@ -0,0 +1,206 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { + GeoPoint, GeoPointInterface, MapRotation, MapRotationModule, MapSystemController, MapSystemKeys, ResourceConsumer, ResourceModerator, + Subject, Subscription +} from '@microsoft/msfs-sdk'; + +import { GarminMapKeys } from '../GarminMapKeys'; +import { MapResourcePriority } from '../MapResourcePriority'; +import { MapPanningModule } from '../modules/MapPanningModule'; + +/** + * Modules required for MapPanningRTRController. + */ +export interface MapPanningRTRControllerModules { + /** Panning module. */ + [GarminMapKeys.Panning]: MapPanningModule; + + /** Rotation module. */ + [MapSystemKeys.Rotation]?: MapRotationModule; +} + +/** + * Context properties required for MapPanningRTRController. + */ +export interface MapPanningRTRControllerContext { + /** Resource moderator for control of the map's projection target. */ + [MapSystemKeys.TargetControl]?: ResourceModerator; + + /** Resource moderator for control of the map's rotation mode. */ + [GarminMapKeys.RotationModeControl]?: ResourceModerator; + + /** Resource moderator for control of the map's orientation mode. */ + [GarminMapKeys.OrientationControl]?: ResourceModerator; + + /** Resource moderator for control of the map's range. */ + [MapSystemKeys.RangeControl]?: ResourceModerator; + + /** Resource moderator for the use range setting subject. */ + [GarminMapKeys.UseRangeSetting]?: ResourceModerator>; +} + +/** + * Controls the target, orientation, and range of a map while manual map panning is active. + */ +export class MapPanningRTRController extends MapSystemController { + private readonly panningModule = this.context.model.getModule(GarminMapKeys.Panning); + private readonly rotationModule = this.context.model.getModule(MapSystemKeys.Rotation); + + private readonly mapProjectionParams = { + target: new GeoPoint(0, 0) + }; + + private readonly targetControl = this.context[MapSystemKeys.TargetControl]; + + private hasTargetControl = this.context.targetControlModerator === undefined; + + private readonly targetControlConsumer: ResourceConsumer = { + priority: MapResourcePriority.PANNING, + + onAcquired: () => { + this.hasTargetControl = true; + + if (this.panningModule.isActive.get()) { + this.setMapTarget(this.panningModule.target.get()); + } + }, + + onCeded: () => { + this.hasTargetControl = false; + } + }; + + private readonly rotationModeControl = this.context[GarminMapKeys.RotationModeControl]; + + private readonly rotationModeControlConsumer: ResourceConsumer = { + priority: MapResourcePriority.PANNING, + + onAcquired: () => { + // While panning is active, the map keeps its rotation from when panning was activated. + this.rotationModule?.rotationType.set(MapRotation.Undefined); + }, + + onCeded: () => { } + }; + + private readonly orientationControl = this.context[GarminMapKeys.OrientationControl]; + + private readonly orientationControlConsumer: ResourceConsumer = { + priority: MapResourcePriority.PANNING, + + onAcquired: () => { }, // While panning is active, the map keeps its desired orientation mode from when panning was activated, so we do nothing. + + onCeded: () => { } + }; + + private readonly rangeControl = this.context[MapSystemKeys.RangeControl]; + + private readonly rangeControlConsumer: ResourceConsumer = { + priority: MapResourcePriority.PANNING, + + onAcquired: () => { }, // We are just holding this to keep other things of lower priority from changing the range. + + onCeded: () => { } + }; + + private readonly useRangeSetting = this.context[GarminMapKeys.UseRangeSetting]; + + private readonly useRangeSettingConsumer: ResourceConsumer> = { + priority: MapResourcePriority.PANNING, + + onAcquired: () => { }, // Panning mode uses the use range setting state that was active when panning was activated, so we do nothing while we have control. + + onCeded: () => { } + }; + + private panningActiveSub?: Subscription; + private panningTargetSub?: Subscription; + + /** @inheritdoc */ + public onAfterMapRender(): void { + this.panningTargetSub = this.panningModule.target.sub(this.onTargetChanged.bind(this), false, true); + this.panningActiveSub = this.panningModule.isActive.sub(this.onPanningActiveChanged.bind(this), true); + } + + /** + * Responds to map panning activation changes. + * @param isActive Whether map panning is active. + */ + private onPanningActiveChanged(isActive: boolean): void { + if (isActive) { + this.onPanningActivated(); + } else { + this.onPanningDeactivated(); + } + } + + /** + * Responds to map panning activation. + */ + private onPanningActivated(): void { + this.targetControl?.claim(this.targetControlConsumer); + if (this.rotationModeControl) { + this.rotationModeControl.claim(this.rotationModeControlConsumer); + } else if (this.rotationModule) { + // While panning is active, the map keeps its rotation from when panning was activated. + this.rotationModule.rotationType.set(MapRotation.Undefined); + } + this.orientationControl?.claim(this.orientationControlConsumer); + this.rangeControl?.claim(this.rangeControlConsumer); + this.useRangeSetting?.claim(this.useRangeSettingConsumer); + + this.panningTargetSub!.resume(); + } + + /** + * Responds to map panning deactivation. + */ + private onPanningDeactivated(): void { + this.panningTargetSub!.pause(); + + this.targetControl?.forfeit(this.targetControlConsumer); + this.rotationModeControl?.forfeit(this.rotationModeControlConsumer); + this.orientationControl?.forfeit(this.orientationControlConsumer); + this.rangeControl?.forfeit(this.rangeControlConsumer); + this.useRangeSetting?.forfeit(this.useRangeSettingConsumer); + } + + /** + * Responds to when the map panning target changes. + * @param target The new map panning target. + */ + private onTargetChanged(target: GeoPointInterface): void { + if (this.hasTargetControl) { + this.setMapTarget(target); + } + } + + /** + * Sets the map projection's target. + * @param target The target to set. + */ + private setMapTarget(target: GeoPointInterface): void { + this.mapProjectionParams.target.set(target); + this.context.projection.setQueued(this.mapProjectionParams); + } + + /** @inheritdoc */ + public onMapDestroyed(): void { + this.destroy(); + } + + /** @inheritdoc */ + public destroy(): void { + this.targetControl?.forfeit(this.targetControlConsumer); + this.rotationModeControl?.forfeit(this.rotationModeControlConsumer); + this.orientationControl?.forfeit(this.orientationControlConsumer); + this.rangeControl?.forfeit(this.rangeControlConsumer); + this.useRangeSetting?.forfeit(this.useRangeSettingConsumer); + + this.panningActiveSub?.destroy(); + this.panningTargetSub?.destroy(); + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/garminsdk/components/map/controllers/MapPointerRTRController.ts b/src/garminsdk/components/map/controllers/MapPointerRTRController.ts index a3a6af46c..657cf5a8d 100644 --- a/src/garminsdk/components/map/controllers/MapPointerRTRController.ts +++ b/src/garminsdk/components/map/controllers/MapPointerRTRController.ts @@ -1,12 +1,12 @@ import { - BitFlags, GeoPoint, GeoPointInterface, MapProjection, MapProjectionChangeType, MapRotation, MapRotationModule, MapSystemContext, MapSystemController, - MapSystemKeys, MathUtils, ReadonlyFloat64Array, ResourceConsumer, ResourceModerator, Subject, Subscribable, Subscription, Vec2Math, + BitFlags, GeoPoint, MapProjection, MapProjectionChangeType, MapSystemContext, MapSystemController, + MathUtils, ReadonlyFloat64Array, Subscribable, SubscribableUtils, Subscription, Vec2Math, VecNMath, VecNSubject } from '@microsoft/msfs-sdk'; import { GarminMapKeys } from '../GarminMapKeys'; -import { MapResourcePriority } from '../MapResourcePriority'; import { MapOrientationModule } from '../modules/MapOrientationModule'; +import { MapPanningModule } from '../modules/MapPanningModule'; import { MapPointerModule } from '../modules/MapPointerModule'; /** @@ -16,8 +16,8 @@ export interface MapPointerRTRControllerModules { /** Pointer module. */ [GarminMapKeys.Pointer]: MapPointerModule; - /** Rotation module. */ - [MapSystemKeys.Rotation]?: MapRotationModule; + /** Panning module. */ + [GarminMapKeys.Panning]: MapPanningModule; /** Orientation module. */ [GarminMapKeys.Orientation]?: MapOrientationModule; @@ -26,119 +26,60 @@ export interface MapPointerRTRControllerModules { /** * Context properties required for MapPointerRTRController. */ -export interface MapPointerRTRControllerContext { - /** Resource moderator for control of the map's projection target. */ - [MapSystemKeys.TargetControl]?: ResourceModerator; - - /** Resource moderator for control of the map's rotation mode. */ - [GarminMapKeys.RotationModeControl]?: ResourceModerator; - - /** Resource moderator for control of the map's range. */ - [MapSystemKeys.RangeControl]?: ResourceModerator; - - /** Resource moderator for the use range setting subject. */ - [GarminMapKeys.UseRangeSetting]?: ResourceModerator>; -} +export type MapPointerRTRControllerContext = Record; /** - * Controls the pointer of a map. + * Controls the target, orientation, and range of a map while the map pointer is active. */ -export class MapPointerRTRController extends MapSystemController { +export class MapPointerRTRController extends MapSystemController { private readonly pointerModule = this.context.model.getModule(GarminMapKeys.Pointer); - private readonly rotationModule = this.context.model.getModule(MapSystemKeys.Rotation); + private readonly panningModule = this.context.model.getModule(GarminMapKeys.Panning); private readonly orientationModule = this.context.model.getModule(GarminMapKeys.Orientation); - private readonly mapProjectionParams = { - target: new GeoPoint(0, 0) - }; - - private readonly targetControl = this.context[MapSystemKeys.TargetControl]; - - private hasTargetControl = this.context.targetControlModerator === undefined; - - private readonly targetControlConsumer: ResourceConsumer = { - priority: MapResourcePriority.POINTER, - - onAcquired: () => { - this.hasTargetControl = true; - }, - - onCeded: () => { - this.hasTargetControl = false; - } - }; - - private readonly rotationModeControl = this.context[GarminMapKeys.RotationModeControl]; - - private readonly rotationModeControlConsumer: ResourceConsumer = { - priority: MapResourcePriority.POINTER, - - onAcquired: () => { - // While pointer is active, the map keeps its rotation from when the pointer was activated. - this.rotationModule?.rotationType.set(MapRotation.Undefined); - }, + protected readonly pointerBoundsOffset: Subscribable; - onCeded: () => { } - }; - - private readonly rangeControl = this.context[MapSystemKeys.RangeControl]; - - private readonly rangeControlConsumer: ResourceConsumer = { - priority: MapResourcePriority.POINTER, - - onAcquired: () => { }, // We are just holding this to keep other things of lower priority from changing the range. - - onCeded: () => { } - }; - - private readonly useRangeSetting = this.context[GarminMapKeys.UseRangeSetting]; - - private readonly useRangeSettingConsumer: ResourceConsumer> = { - priority: MapResourcePriority.POINTER, - - onAcquired: () => { }, // Pointer mode uses the use range setting state that was active when the pointer was activated, so we do nothing while we have control. - - onCeded: () => { } - }; - - private readonly pointerBounds = VecNSubject.createFromVector(VecNMath.create(4)); + private readonly pointerBounds = VecNSubject.create(VecNMath.create(4)); private needUpdatePointerScroll = false; + private targetPipe?: Subscription; + private pointerBoundsOffsetSub?: Subscription; private pointerActiveSub?: Subscription; private pointerBoundsSub?: Subscription; private pointerPositionSub?: Subscription; - private pointerTargetSub?: Subscription; - private orientationSub?: Subscription; + private commandedOrientationSub?: Subscription; /** - * Constructor. + * Creates a new instance of MapPointerRTRController. * @param context This controller's map context. - * @param pointerBoundsOffset A subscribable which provides the offset of the boundary surrounding the area in which - * the pointer can freely move, from the edge of the projected map, excluding the dead zone. Expressed as - * `[left, top, right, bottom]`, relative to the width and height, as appropriate, of the projected map. A positive - * offset is directed toward the center of the map. + * @param pointerBoundsOffset The offset of the boundary surrounding the area in which the pointer can freely move, + * from the edge of the projected map, excluding the dead zone. Expressed as `[left, top, right, bottom]`, relative + * to the width and height, as appropriate, of the projected map. A positive offset is directed toward the center of + * the map. */ constructor( - context: MapSystemContext, - protected readonly pointerBoundsOffset: Subscribable + context: MapSystemContext, + pointerBoundsOffset: ReadonlyFloat64Array | Subscribable ) { super(context); + + this.pointerBoundsOffset = SubscribableUtils.toSubscribable(pointerBoundsOffset, true); } /** @inheritdoc */ public onAfterMapRender(): void { + this.targetPipe = this.pointerModule.target.pipe(this.panningModule.target, true); + this.pointerBoundsSub = this.pointerBounds.sub(this.onPointerBoundsChanged.bind(this), false, true); this.pointerPositionSub = this.pointerModule.position.sub(this.onPointerPositionChanged.bind(this), false, true); - this.pointerTargetSub = this.pointerModule.target.sub(this.onPointerTargetChanged.bind(this), false, true); this.pointerBoundsOffsetSub = this.pointerBoundsOffset.sub(this.updatePointerBounds.bind(this), true); this.pointerActiveSub = this.pointerModule.isActive.sub(this.onPointerActiveChanged.bind(this), true); - this.orientationSub = this.orientationModule?.orientation.sub(() => { this.pointerModule.isActive.set(false); }, false, true); + this.commandedOrientationSub = this.orientationModule?.commandedOrientation.sub(() => { this.pointerModule.isActive.set(false); }, false, true); } /** @@ -171,8 +112,6 @@ export class MapPointerRTRController extends MapSystemController; +}; + /** * Controls the display of terrain based on user settings. */ @@ -48,11 +62,14 @@ export class MapTerrainController extends MapSystemController; private readonly showScaleSetting?: Subscribable; - private terrainModeState?: MappedSubscribable; + private readonly allowRelative: boolean; + private readonly defaultTerrainMode: Subscribable; + + private terrainModeState?: MappedSubscribable; private showScalePipe?: Subscription; /** - * Constructor. + * Creates a new instance of MapTerrainController. * @param context This controller's map context. * @param settingManager A setting manager containing the user settings controlling the display of terrain. If not * defined, the display of terrain will not be bound to user settings. @@ -62,10 +79,33 @@ export class MapTerrainController extends MapSystemController, settingManager?: UserSettingManager>, - private readonly allowRelative = true + allowRelative?: boolean + ); + /** + * Creates a new instance of MapTerrainController. + * @param context This controller's map context. + * @param settingManager A setting manager containing the user settings controlling the display of terrain. If not + * defined, the display of terrain will not be bound to user settings. + * @param options Options with which to configure the controller. + */ + constructor( + context: MapSystemContext, + settingManager?: UserSettingManager>, + options?: Readonly + ); + // eslint-disable-next-line jsdoc/require-jsdoc + constructor( + context: MapSystemContext, + settingManager?: UserSettingManager>, + arg3?: boolean | Readonly ) { super(context); + const options = arg3 === undefined ? undefined : typeof arg3 === 'object' ? arg3 : { allowRelative: arg3 }; + + this.allowRelative = options?.allowRelative ?? true; + this.defaultTerrainMode = SubscribableUtils.toSubscribable(options?.defaultMode ?? MapTerrainMode.None, true); + this.modeSetting = settingManager?.tryGetSetting('mapTerrainMode'); this.rangeIndexSetting = settingManager?.tryGetSetting('mapTerrainRangeIndex') ?? Subject.create(Number.MAX_SAFE_INTEGER); this.showScaleSetting = settingManager?.tryGetSetting('mapTerrainScaleShow'); @@ -75,6 +115,7 @@ export class MapTerrainController extends MapSystemController { + this.terrainModeState.sub(([defaultMode, modeSetting, rangeIndexSetting, rangeIndex, isOnGround, isGpsDataValid]): void => { let mode = MapTerrainMode.None; let isRelativeFailed = false; @@ -96,6 +137,7 @@ export class MapTerrainController extends MapSystemController, - settingManager: UserSettingManager> + settingManager: UserSettingManager>, + options?: Readonly ) { super(context); @@ -68,7 +117,7 @@ export class MapWaypointsVisController extends MapSystemController { - private static readonly MODE_MAP = { + private static readonly MODE_MAP: Partial> = { [MapOrientationSettingMode.NorthUp]: MapOrientation.NorthUp, [MapOrientationSettingMode.HeadingUp]: MapOrientation.HeadingUp, - [MapOrientationSettingMode.TrackUp]: MapOrientation.TrackUp + [MapOrientationSettingMode.TrackUp]: MapOrientation.TrackUp, + [MapOrientationSettingMode.DtkUp]: MapOrientation.DtkUp }; - private static readonly WEATHER_MODE_MAP = { + private static readonly WEATHER_MODE_MAP: Partial> = { [WeatherMapOrientationSettingMode.NorthUp]: MapOrientation.NorthUp, [WeatherMapOrientationSettingMode.HeadingUp]: MapOrientation.HeadingUp, [WeatherMapOrientationSettingMode.TrackUp]: MapOrientation.TrackUp, - [WeatherMapOrientationSettingMode.SyncToNavMap]: MapOrientation.HeadingUp + [WeatherMapOrientationSettingMode.DtkUp]: MapOrientation.DtkUp }; private readonly rangeModule = this.context.model.getModule(GarminMapKeys.Range); diff --git a/src/garminsdk/components/map/controllers/WeatherMapOrientationSettingsController.ts b/src/garminsdk/components/map/controllers/WeatherMapOrientationSettingsController.ts new file mode 100644 index 000000000..4d36b6f4f --- /dev/null +++ b/src/garminsdk/components/map/controllers/WeatherMapOrientationSettingsController.ts @@ -0,0 +1,138 @@ +import { + MapSystemContext, MapSystemController, MappedSubject, Subscription, UserSettingManager +} from '@microsoft/msfs-sdk'; + +import { MapOrientationSettingMode, MapUserSettingTypes } from '../../../settings/MapUserSettings'; +import { WeatherMapOrientationSettingMode, WeatherMapUserSettingTypes } from '../../../settings/WeatherMapUserSettings'; +import { GarminMapKeys } from '../GarminMapKeys'; +import { MapOrientation, MapOrientationModule } from '../modules/MapOrientationModule'; + +/** + * Modules required for WeatherMapOrientationSettingsController. + */ +export interface WeatherMapOrientationSettingsControllerModules { + /** Orientation module. */ + [GarminMapKeys.Orientation]: MapOrientationModule; +} + +/** + * User settings required by WeatherMapOrientationSettingsController. + */ +export type WeatherMapOrientationSettingsControllerSettings = Pick< + MapUserSettingTypes & WeatherMapUserSettingTypes, + 'weatherMapOrientation' | 'mapOrientation' | 'mapAutoNorthUpActive' | 'mapAutoNorthUpRangeIndex' +>; + +/** + * Controls the orientation of a weather map based on user settings. + */ +export class WeatherMapOrientationSettingsController extends MapSystemController { + private static readonly MODE_MAP: Partial> = { + [MapOrientationSettingMode.NorthUp]: MapOrientation.NorthUp, + [MapOrientationSettingMode.HeadingUp]: MapOrientation.HeadingUp, + [MapOrientationSettingMode.TrackUp]: MapOrientation.TrackUp, + [MapOrientationSettingMode.DtkUp]: MapOrientation.DtkUp + }; + private static readonly WEATHER_MODE_MAP: Partial> = { + [WeatherMapOrientationSettingMode.NorthUp]: MapOrientation.NorthUp, + [WeatherMapOrientationSettingMode.HeadingUp]: MapOrientation.HeadingUp, + [WeatherMapOrientationSettingMode.TrackUp]: MapOrientation.TrackUp, + [WeatherMapOrientationSettingMode.DtkUp]: MapOrientation.DtkUp + }; + + private readonly orientationModule = this.context.model.getModule(GarminMapKeys.Orientation); + + private readonly subs: Subscription[] = []; + + /** + * Creates a new instance of WeatherMapOrientationSettingsController. + * @param context This controller's map context. + * @param settingManager The setting manager used by this controller. + */ + public constructor( + context: MapSystemContext, + private readonly settingManager: UserSettingManager> + ) { + super(context); + } + + /** @inheritdoc */ + public onAfterMapRender(): void { + const weatherMapOrientationSetting = this.settingManager.tryGetSetting('weatherMapOrientation'); + + if (weatherMapOrientationSetting) { + const orientationSetting = this.settingManager.tryGetSetting('mapOrientation'); + + if (orientationSetting) { + const desiredOrientation = MappedSubject.create( + ([weatherOrientation, orientation]): MapOrientation => { + return weatherOrientation === WeatherMapOrientationSettingMode.SyncToNavMap + ? WeatherMapOrientationSettingsController.MODE_MAP[orientation] ?? MapOrientation.NorthUp + : WeatherMapOrientationSettingsController.WEATHER_MODE_MAP[weatherOrientation] ?? MapOrientation.NorthUp; + }, + weatherMapOrientationSetting, + orientationSetting, + ); + + this.subs.push( + desiredOrientation, + desiredOrientation.pipe(this.orientationModule.commandedOrientation) + ); + } else { + this.subs.push(weatherMapOrientationSetting.pipe(this.orientationModule.commandedOrientation, setting => { + return WeatherMapOrientationSettingsController.WEATHER_MODE_MAP[setting] ?? MapOrientation.NorthUp; + })); + } + + const northUpAboveActiveSetting = this.settingManager.tryGetSetting('mapAutoNorthUpActive'); + if (northUpAboveActiveSetting) { + const weatherNorthUpAboveActive = MappedSubject.create( + ([weatherOrientation, northUpAboveActive]) => { + return weatherOrientation === WeatherMapOrientationSettingMode.SyncToNavMap + ? northUpAboveActive === true + : false; + }, + weatherMapOrientationSetting, + northUpAboveActiveSetting, + ); + + this.subs.push( + weatherNorthUpAboveActive, + weatherNorthUpAboveActive.pipe(this.orientationModule.northUpAboveActive) + ); + } + + const northUpAboveRangeIndexSetting = this.settingManager.tryGetSetting('mapAutoNorthUpRangeIndex'); + if (northUpAboveRangeIndexSetting) { + const weatherNorthUpAboveRangeIndex = MappedSubject.create( + ([weatherOrientation, northUpAboveRangeIndex]) => { + return weatherOrientation === WeatherMapOrientationSettingMode.SyncToNavMap && typeof northUpAboveRangeIndex === 'number' + ? northUpAboveRangeIndex + : Infinity; + }, + weatherMapOrientationSetting, + northUpAboveRangeIndexSetting, + ); + + this.subs.push( + weatherNorthUpAboveRangeIndex, + weatherNorthUpAboveRangeIndex.pipe(this.orientationModule.northUpAboveRangeIndex) + ); + } + } + } + + /** @inheritdoc */ + public onMapDestroyed(): void { + this.destroy(); + } + + /** @inheritdoc */ + public destroy(): void { + for (const sub of this.subs) { + sub.destroy(); + } + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/garminsdk/components/map/controllers/index.ts b/src/garminsdk/components/map/controllers/index.ts index 1a25aadda..29638ce88 100644 --- a/src/garminsdk/components/map/controllers/index.ts +++ b/src/garminsdk/components/map/controllers/index.ts @@ -1,11 +1,15 @@ export * from './MapAirspaceVisController'; export * from './MapDataIntegrityRTRController'; +export * from './MapDesiredOrientationController'; export * from './MapFlightPlanFocusRTRController'; export * from './MapGarminAutopilotPropsController'; export * from './MapGarminTrafficController'; export * from './MapNexradController'; export * from './MapOrientationController'; +export * from './MapOrientationModeController'; export * from './MapOrientationRTRController'; +export * from './MapOrientationSettingsController'; +export * from './MapPanningRTRController'; export * from './MapPointerController'; export * from './MapPointerRTRController'; export * from './MapRangeCompassController'; @@ -22,4 +26,5 @@ export * from './NearestMapRTRController'; export * from './TrafficMapRangeController'; export * from './WaypointMapHighlightController'; export * from './WaypointMapRTRController'; -export * from './WeatherMapOrientationController'; \ No newline at end of file +export * from './WeatherMapOrientationController'; +export * from './WeatherMapOrientationSettingsController'; \ No newline at end of file diff --git a/src/garminsdk/components/map/index.ts b/src/garminsdk/components/map/index.ts index b560bd5e7..691d289cc 100644 --- a/src/garminsdk/components/map/index.ts +++ b/src/garminsdk/components/map/index.ts @@ -23,4 +23,5 @@ export * from './MapUtils'; export * from './MapWaypointDisplayBuilder'; export * from './MapWaypointIcon'; export * from './MapWaypointRenderer'; -export * from './MapWaypointStyles'; \ No newline at end of file +export * from './MapWaypointStyles'; +export * from './NextGenGarminMapBuilder'; \ No newline at end of file diff --git a/src/garminsdk/components/map/indicators/MapRelativeTerrainStatusIndicator.tsx b/src/garminsdk/components/map/indicators/MapRelativeTerrainStatusIndicator.tsx index 8d5efe150..abf261d5f 100644 --- a/src/garminsdk/components/map/indicators/MapRelativeTerrainStatusIndicator.tsx +++ b/src/garminsdk/components/map/indicators/MapRelativeTerrainStatusIndicator.tsx @@ -41,9 +41,9 @@ export class MapRelativeTerrainStatusIndicator extends DisplayComponent

- - - + + +
diff --git a/src/garminsdk/components/map/layers/MapRangeCompassLayer.tsx b/src/garminsdk/components/map/layers/MapRangeCompassLayer.tsx index 63a2d1918..8b37cbb29 100644 --- a/src/garminsdk/components/map/layers/MapRangeCompassLayer.tsx +++ b/src/garminsdk/components/map/layers/MapRangeCompassLayer.tsx @@ -183,6 +183,8 @@ export class MapRangeCompassLayer extends MapLayer { private static readonly vec2Cache = Array.from({ length: 4 }, () => new Float64Array(2)); + private thisNode?: VNode; + private readonly rootRef = FSComponent.createRef(); private readonly arcLayerRef = FSComponent.createRef>>(); private readonly roseLayerContainerRef = FSComponent.createRef(); @@ -240,6 +242,13 @@ export class MapRangeCompassLayer extends MapLayer { private needUpdateHeadingIndicatorVisibility = true; private needRepositionLabel = true; + private readonly subscriptions: Subscription[] = []; + + /** @inheritdoc */ + public onAfterRender(thisNode: VNode): void { + this.thisNode = thisNode; + } + /** @inheritdoc */ public onVisibilityChanged(isVisible: boolean): void { this.needUpdateRootVisibility = true; @@ -280,40 +289,30 @@ export class MapRangeCompassLayer extends MapLayer { * Initializes listeners. */ private initListeners(): void { - this.initParameterListeners(); - this.initModuleListeners(); - this.isFollowingAirplane.sub(() => { - this.needRechooseReferenceMarker = true; - this.needUpdateHeadingIndicatorVisibility = true; - }); - } - - /** - * Initializes parameter listeners. - */ - private initParameterListeners(): void { this.centerSubject.sub(this.onCenterChanged.bind(this)); this.radiusSubject.sub(this.onRadiusChanged.bind(this)); this.rotationSubject.sub(this.onRotationChanged.bind(this)); this.magVarCorrectionSubject.sub(this.onMagVarCorrectionChanged.bind(this)); - } - /** - * Initializes modules listeners. - */ - private initModuleListeners(): void { - this.rangeModule.nominalRange.sub(this.onRangeChanged.bind(this)); - this.orientationModule.orientation.sub(this.onOrientationChanged.bind(this)); - this.rangeCompassModule.show.sub(this.onRangeCompassShowChanged.bind(this)); + this.subscriptions.push( + this.rangeModule.nominalRange.sub(this.onRangeChanged.bind(this)), + + this.orientationModule.orientation.sub(this.onOrientationChanged.bind(this)), + + this.rangeCompassModule.show.sub(this.onRangeCompassShowChanged.bind(this)), + + this.isFollowingAirplane.sub(() => { + this.needRechooseReferenceMarker = true; + this.needUpdateHeadingIndicatorVisibility = true; + }) + ); } /** @inheritdoc */ public onMapProjectionChanged(mapProjection: MapProjection, changeFlags: number): void { this.arcLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags); this.roseLabelsLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags); - if (this.props.showHeadingBug) { - this.headingIndicatorRef.instance.onMapProjectionChanged(mapProjection, changeFlags); - } + this.headingIndicatorRef.getOrDefault()?.onMapProjectionChanged(mapProjection, changeFlags); if (BitFlags.isAll(changeFlags, MapProjectionChangeType.ProjectedSize)) { // resizing the map will cause synced canvas layers to clear themselves, so we need to force a redraw on these @@ -564,9 +563,7 @@ export class MapRangeCompassLayer extends MapLayer { this.roseLayerRef.instance.onUpdated(time, elapsed); this.roseLabelsLayerRef.instance.onUpdated(time, elapsed); this.referenceMarkerContainerRef.instance.onUpdated(time, elapsed); - if (this.props.showHeadingBug) { - this.headingIndicatorRef.instance.onUpdated(time, elapsed); - } + this.headingIndicatorRef.getOrDefault()?.onUpdated(time, elapsed); } /** @@ -736,6 +733,17 @@ export class MapRangeCompassLayer extends MapLayer { ) : null; } + + /** @inheritdoc */ + public destroy(): void { + this.thisNode && FSComponent.shallowDestroy(this.thisNode); + + for (const sub of this.subscriptions) { + sub.destroy(); + } + + super.destroy(); + } } /** @@ -1633,6 +1641,8 @@ class MapRangeCompassSelectedHeading extends MapLayer { return this.isUserVisible; } } else if (waypoint instanceof MapRunwayLabelWaypoint) { - return this.waypointsModule.runwayShow.get() + return this.waypointsModule.runwayLabelShow.get() && UnitType.METER.convertTo(waypoint.runway.length, UnitType.GA_RADIAN) / this.props.mapProjection.getProjectedResolution() >= this.waypointsModule.runwayLabelMinLength.get(); } else if (waypoint instanceof MapRunwayOutlineWaypoint) { diff --git a/src/garminsdk/components/map/layers/TrafficMapRangeLayer.tsx b/src/garminsdk/components/map/layers/TrafficMapRangeLayer.tsx index 34ea8bcf0..42faaf727 100644 --- a/src/garminsdk/components/map/layers/TrafficMapRangeLayer.tsx +++ b/src/garminsdk/components/map/layers/TrafficMapRangeLayer.tsx @@ -1,6 +1,8 @@ import { - BitFlags, FSComponent, MapIndexedRangeModule, MapLabeledRingLabel, MapLabeledRingLayer, MapLayer, MapLayerProps, MapProjection, MapProjectionChangeType, - MapSyncedCanvasLayer, NumberUnitInterface, NumberUnitSubject, ReadonlyFloat64Array, Subject, Subscribable, Unit, UnitFamily, UnitType, Vec2Math, VNode + BitFlags, FSComponent, GenericMapSharedCanvasSubLayer, MapIndexedRangeModule, MapLabeledRingCanvasSubLayer, + MapLabeledRingLabel, MapLayer, MapLayerProps, MapProjection, MapProjectionChangeType, MapSharedCanvasLayer, + MapSyncedCanvasLayer, MathUtils, NumberUnitInterface, NumberUnitSubject, ReadonlyFloat64Array, Subject, Subscribable, Unit, + UnitFamily, UnitType, VNode, Vec2Math } from '@microsoft/msfs-sdk'; import { GarminMapKeys } from '../GarminMapKeys'; @@ -36,6 +38,15 @@ export interface TrafficMapRangeLayerProps extends MapLayerProps { private static readonly DEFAULT_STROKE_WIDTH = 2; private static readonly DEFAULT_STROKE_STYLE = 'white'; private static readonly DEFAULT_STROKE_DASH = [4, 4]; + + private static readonly DEFAULT_OUTLINE_WIDTH = 0; + private static readonly DEFAULT_OUTLINE_STYLE = 'black'; + private static readonly DEFAULT_OUTLINE_DASH = []; + private static readonly DEFAULT_TICK_COLOR = 'white'; private static readonly DEFAULT_MAJOR_TICK_SIZE = 10; private static readonly DEFAULT_MINOR_TICK_SIZE = 5; private static readonly vec2Cache = [new Float64Array(2)]; + private readonly canvasLayerRef = FSComponent.createRef(); private readonly tickLayerRef = FSComponent.createRef>(); - private readonly innerRangeLayerRef = FSComponent.createRef>(); - private readonly outerRangeLayerRef = FSComponent.createRef>(); + private readonly innerRingLayerRef = FSComponent.createRef>(); + private readonly outerRingLayerRef = FSComponent.createRef>(); private readonly outerStrokeWidth = this.props.outerStrokeWidth ?? TrafficMapRangeLayer.DEFAULT_STROKE_WIDTH; private readonly outerStrokeStyle = this.props.outerStrokeStyle ?? TrafficMapRangeLayer.DEFAULT_STROKE_STYLE; private readonly outerStrokeDash = this.props.outerStrokeDash ?? TrafficMapRangeLayer.DEFAULT_STROKE_DASH; + private readonly outerOutlineWidth = this.props.outerOutlineWidth ?? TrafficMapRangeLayer.DEFAULT_OUTLINE_WIDTH; + private readonly outerOutlineStyle = this.props.outerOutlineStyle ?? TrafficMapRangeLayer.DEFAULT_OUTLINE_STYLE; + private readonly outerOutlineDash = this.props.outerOutlineDash ?? TrafficMapRangeLayer.DEFAULT_OUTLINE_DASH; + private readonly innerStrokeWidth = this.props.innerStrokeWidth ?? TrafficMapRangeLayer.DEFAULT_STROKE_WIDTH; private readonly innerStrokeStyle = this.props.innerStrokeStyle ?? TrafficMapRangeLayer.DEFAULT_STROKE_STYLE; private readonly innerStrokeDash = this.props.innerStrokeDash ?? TrafficMapRangeLayer.DEFAULT_STROKE_DASH; + private readonly innerOutlineWidth = this.props.innerOutlineWidth ?? TrafficMapRangeLayer.DEFAULT_OUTLINE_WIDTH; + private readonly innerOutlineStyle = this.props.innerOutlineStyle ?? TrafficMapRangeLayer.DEFAULT_OUTLINE_STYLE; + private readonly innerOutlineDash = this.props.innerOutlineDash ?? TrafficMapRangeLayer.DEFAULT_OUTLINE_DASH; - private readonly tickColor = this.props.tickColor ?? TrafficMapRangeLayer.DEFAULT_TICK_COLOR; + private readonly outerMajorTickColor = this.props.outerMajorTickColor ?? this.props.tickColor ?? TrafficMapRangeLayer.DEFAULT_TICK_COLOR; private readonly outerMajorTickSize = this.props.outerMajorTickSize ?? TrafficMapRangeLayer.DEFAULT_MAJOR_TICK_SIZE; + private readonly outerMinorTickColor = this.props.outerMinorTickColor ?? this.props.tickColor ?? TrafficMapRangeLayer.DEFAULT_TICK_COLOR; private readonly outerMinorTickSize = this.props.outerMinorTickSize ?? TrafficMapRangeLayer.DEFAULT_MINOR_TICK_SIZE; + private readonly innerMajorTickColor = this.props.innerMajorTickColor ?? this.props.tickColor ?? TrafficMapRangeLayer.DEFAULT_TICK_COLOR; private readonly innerMajorTickSize = this.props.innerMajorTickSize ?? TrafficMapRangeLayer.DEFAULT_MAJOR_TICK_SIZE; + private readonly innerMinorTickColor = this.props.innerMinorTickColor ?? this.props.tickColor ?? TrafficMapRangeLayer.DEFAULT_TICK_COLOR; private readonly innerMinorTickSize = this.props.innerMinorTickSize ?? TrafficMapRangeLayer.DEFAULT_MINOR_TICK_SIZE; private readonly rangeModule = this.props.model.getModule(GarminMapKeys.Range); private readonly trafficModule = this.props.model.getModule(GarminMapKeys.Traffic); - private readonly innerRange = NumberUnitSubject.createFromNumberUnit(UnitType.NMILE.createNumber(0)); - private readonly outerRange = NumberUnitSubject.createFromNumberUnit(UnitType.NMILE.createNumber(0)); + private readonly innerRange = NumberUnitSubject.create(UnitType.NMILE.createNumber(0)); + private readonly outerRange = NumberUnitSubject.create(UnitType.NMILE.createNumber(0)); + + private innerRadius = 0; + private outerRadius = 0; private innerLabel: MapLabeledRingLabel | null = null; private outerLabel: MapLabeledRingLabel | null = null; private needUpdateRings = false; + private needUpdateTicks = false; /** @inheritdoc */ public onAttached(): void { - this.tickLayerRef.instance.onAttached(); - this.innerRangeLayerRef.instance.onAttached(); - this.outerRangeLayerRef.instance.onAttached(); + this.canvasLayerRef.instance.onAttached(); this.initLabels(); this.initStyles(); @@ -142,7 +192,7 @@ export class TrafficMapRangeLayer extends MapLayer { const displayUnit = Subject.create(UnitType.NMILE); if (this.props.innerLabelRadial !== null && this.props.innerLabelRadial !== undefined) { - this.innerLabel = this.innerRangeLayerRef.instance.createLabel( + this.innerLabel = this.innerRingLayerRef.instance.createLabel( this.props.renderLabel !== undefined ? this.props.renderLabel(this.innerRange, displayUnit) : () @@ -153,7 +203,7 @@ export class TrafficMapRangeLayer extends MapLayer { } if (this.props.outerLabelRadial !== null && this.props.outerLabelRadial !== undefined) { - this.outerLabel = this.outerRangeLayerRef.instance.createLabel( + this.outerLabel = this.outerRingLayerRef.instance.createLabel( this.props.renderLabel !== undefined ? this.props.renderLabel(this.outerRange, displayUnit) : () @@ -168,10 +218,11 @@ export class TrafficMapRangeLayer extends MapLayer { * Initializes ring styles. */ private initStyles(): void { - this.tickLayerRef.instance.display.context.fillStyle = this.tickColor; + this.innerRingLayerRef.instance.setRingStrokeStyles(this.innerStrokeWidth, this.innerStrokeStyle, this.innerStrokeDash); + this.innerRingLayerRef.instance.setRingOutlineStyles(this.innerOutlineWidth, this.innerOutlineStyle, this.innerOutlineDash); - this.innerRangeLayerRef.instance.setRingStrokeStyles(this.innerStrokeWidth, this.innerStrokeStyle, this.innerStrokeDash); - this.outerRangeLayerRef.instance.setRingStrokeStyles(this.outerStrokeWidth, this.outerStrokeStyle, this.outerStrokeDash); + this.outerRingLayerRef.instance.setRingStrokeStyles(this.outerStrokeWidth, this.outerStrokeStyle, this.outerStrokeDash); + this.outerRingLayerRef.instance.setRingOutlineStyles(this.outerOutlineWidth, this.outerOutlineStyle, this.outerOutlineDash); } /** @@ -190,29 +241,19 @@ export class TrafficMapRangeLayer extends MapLayer { /** @inheritdoc */ public onMapProjectionChanged(mapProjection: MapProjection, changeFlags: number): void { - this.tickLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags); - this.innerRangeLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags); - this.outerRangeLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags); - - if (BitFlags.isAll(changeFlags, MapProjectionChangeType.ProjectedSize)) { - // Need to reset the canvas context fill style because a resize will wipe its state. - this.tickLayerRef.instance.display.context.fillStyle = this.tickColor; - } + this.canvasLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags); - this.needUpdateRings = BitFlags.isAny(changeFlags, MapProjectionChangeType.TargetProjected | MapProjectionChangeType.ProjectedResolution); + this.needUpdateRings ||= BitFlags.isAny(changeFlags, MapProjectionChangeType.TargetProjected | MapProjectionChangeType.ProjectedResolution); } /** @inheritdoc */ public onUpdated(time: number, elapsed: number): void { - this.tickLayerRef.instance.onUpdated(time, elapsed); - if (this.needUpdateRings) { this.updateRings(); this.needUpdateRings = false; } - this.innerRangeLayerRef.instance.onUpdated(time, elapsed); - this.outerRangeLayerRef.instance.onUpdated(time, elapsed); + this.canvasLayerRef.instance.onUpdated(time, elapsed); } /** @@ -220,71 +261,90 @@ export class TrafficMapRangeLayer extends MapLayer { */ private updateRings(): void { const center = this.props.mapProjection.getTargetProjected(); - const innerRadius = (this.innerRange.get().asUnit(UnitType.GA_RADIAN) as number) / this.props.mapProjection.getProjectedResolution(); - const outerRadius = (this.outerRange.get().asUnit(UnitType.GA_RADIAN) as number) / this.props.mapProjection.getProjectedResolution(); + const innerRadius = this.innerRange.get().asUnit(UnitType.GA_RADIAN) / this.props.mapProjection.getProjectedResolution(); + const outerRadius = this.outerRange.get().asUnit(UnitType.GA_RADIAN) / this.props.mapProjection.getProjectedResolution(); if (innerRadius > 0) { - this.innerRangeLayerRef.instance.setVisible(true); - this.innerRangeLayerRef.instance.setRingPosition(center, innerRadius); + this.innerRingLayerRef.instance.setVisible(true); + this.innerRingLayerRef.instance.setRingPosition(center, innerRadius); } else { - this.innerRangeLayerRef.instance.setVisible(false); + this.innerRingLayerRef.instance.setVisible(false); } if (outerRadius > 0) { - this.outerRangeLayerRef.instance.setVisible(true); - this.outerRangeLayerRef.instance.setRingPosition(center, outerRadius); + this.outerRingLayerRef.instance.setVisible(true); + this.outerRingLayerRef.instance.setRingPosition(center, outerRadius); } else { - this.outerRangeLayerRef.instance.setVisible(false); + this.outerRingLayerRef.instance.setVisible(false); } - this.updateTicks(center, innerRadius, outerRadius); + this.innerRadius = innerRadius; + this.outerRadius = outerRadius; + + this.needUpdateTicks = true; } /** - * Updates the ring tick marks. - * @param center The projected center of the rings. - * @param innerRadius The radius of the inner ring, in pixels. - * @param outerRadius The radius of the outer ring, in pixels. + * Updates this layer's ring tick marks. + * @param context A canvas 2D rendering context to which to render the ticks. */ - private updateTicks(center: ReadonlyFloat64Array, innerRadius: number, outerRadius: number): void { - const display = this.tickLayerRef.instance.display; - - display.clear(); + private updateTicks(context: CanvasRenderingContext2D): void { + const center = this.props.mapProjection.getTargetProjected(); - if (innerRadius > 0) { - this.drawTicks(display.context, center, innerRadius, this.innerMajorTickSize, this.innerMinorTickSize); + if (this.innerRadius > 0) { + this.drawTicks(context, center, this.innerRadius, this.innerMajorTickColor, this.innerMajorTickSize, this.innerMinorTickColor, this.innerMinorTickSize); } - if (outerRadius > 0) { - this.drawTicks(display.context, center, outerRadius, this.outerMajorTickSize, this.outerMinorTickSize); + if (this.outerRadius > 0) { + this.drawTicks(context, center, this.outerRadius, this.outerMajorTickColor, this.outerMajorTickSize, this.outerMinorTickColor, this.outerMinorTickSize); } } /** - * Draws ring ticks to a canvas. One major tick is drawn at each of the four cardinal positions, and one minor tick - * is drawn at each of the eight remaining hour positions. + * Draws this layer's ring tick marks to a canvas. One major tick is drawn at each of the four cardinal positions, + * and one minor tick is drawn at each of the eight remaining hour positions. * @param context A canvas 2D rendering context. * @param center The projected center of the outer ring. * @param radius The radius of the ring, in pixels. + * @param majorTickColor The color of each major tick. * @param majorTickSize The size of each major tick, in pixels. + * @param minorTickColor The color of each minor tick. * @param minorTickSize The size of each minor tick, in pixels. */ private drawTicks( context: CanvasRenderingContext2D, center: ReadonlyFloat64Array, radius: number, + majorTickColor: string, majorTickSize: number, + minorTickColor: string, minorTickSize: number ): void { - const step = Math.PI / 6; + // Minor ticks + + context.fillStyle = minorTickColor; + for (let i = 0; i < 12; i++) { - const pos = Vec2Math.setFromPolar(radius, i * step, TrafficMapRangeLayer.vec2Cache[0]); - this.drawTick(context, center[0] + pos[0], center[1] + pos[1], i % 3 === 0 ? majorTickSize : minorTickSize); + if (i % 3 === 0) { + continue; + } + + const pos = Vec2Math.setFromPolar(radius, i * Math.PI / 6, TrafficMapRangeLayer.vec2Cache[0]); + this.drawTick(context, center[0] + pos[0], center[1] + pos[1], minorTickSize); + } + + // Major ticks + + context.fillStyle = majorTickColor; + + for (let i = 0; i < 4; i++) { + const pos = Vec2Math.setFromPolar(radius, i * MathUtils.HALF_PI, TrafficMapRangeLayer.vec2Cache[0]); + this.drawTick(context, center[0] + pos[0], center[1] + pos[1], majorTickSize); } } /** - * Draws a tick to a canvas. + * Draws a ring tick to a canvas. * @param context A canvas 2D rendering context. * @param x The x-coordinate of the center of the tick. * @param y The y-coordinate of the center of the tick. @@ -313,11 +373,38 @@ export class TrafficMapRangeLayer extends MapLayer { /** @inheritdoc */ public render(): VNode { return ( -
- - - -
+ + + + this.needUpdateTicks} + onUpdated={(projection, display) => { + if (display.isInvalidated) { + this.needUpdateTicks = false; + this.updateTicks(display.context); + } + }} + /> + ); } + + /** @inheritdoc */ + public destroy(): void { + this.canvasLayerRef.getOrDefault()?.destroy(); + + super.destroy(); + } } \ No newline at end of file diff --git a/src/garminsdk/components/map/modules/MapOrientationModule.ts b/src/garminsdk/components/map/modules/MapOrientationModule.ts index 20311d2c1..ddcb69673 100644 --- a/src/garminsdk/components/map/modules/MapOrientationModule.ts +++ b/src/garminsdk/components/map/modules/MapOrientationModule.ts @@ -6,13 +6,29 @@ import { Subject } from '@microsoft/msfs-sdk'; export enum MapOrientation { NorthUp, TrackUp, - HeadingUp + HeadingUp, + DtkUp } /** * A module describing the map orientation. */ export class MapOrientationModule { - /** The orientation of the map. */ + /** The actual orientation of the map. */ public readonly orientation = Subject.create(MapOrientation.HeadingUp); + + /** The desired orientation of the map. */ + public readonly desiredOrientation = Subject.create(MapOrientation.HeadingUp); + + /** The map orientation commanded by the user. */ + public readonly commandedOrientation = Subject.create(MapOrientation.HeadingUp); + + /** Whether north up-above is active. */ + public readonly northUpAboveActive = Subject.create(false); + + /** The range index above which north up-above applies. */ + public readonly northUpAboveRangeIndex = Subject.create(Infinity); + + /** Whether north up on ground is active. */ + public readonly northUpOnGroundActive = Subject.create(false); } \ No newline at end of file diff --git a/src/garminsdk/components/map/modules/MapPanningModule.ts b/src/garminsdk/components/map/modules/MapPanningModule.ts new file mode 100644 index 000000000..db8024090 --- /dev/null +++ b/src/garminsdk/components/map/modules/MapPanningModule.ts @@ -0,0 +1,12 @@ +import { GeoPoint, GeoPointSubject, Subject } from '@microsoft/msfs-sdk'; + +/** + * A module describing manual panning of the map. + */ +export class MapPanningModule { + /** Whether panning is active. */ + public readonly isActive = Subject.create(false); + + /** The desired map target. */ + public readonly target = GeoPointSubject.create(new GeoPoint(0, 0)); +} \ No newline at end of file diff --git a/src/garminsdk/components/map/modules/MapPointerModule.ts b/src/garminsdk/components/map/modules/MapPointerModule.ts index aa2e24c63..f50aa5a20 100644 --- a/src/garminsdk/components/map/modules/MapPointerModule.ts +++ b/src/garminsdk/components/map/modules/MapPointerModule.ts @@ -8,8 +8,8 @@ export class MapPointerModule { public readonly isActive = Subject.create(false); /** The position of the pointer on the projected map, in pixel coordinates. */ - public readonly position = Vec2Subject.createFromVector(new Float64Array(2)); + public readonly position = Vec2Subject.create(new Float64Array(2)); /** The desired map target. */ - public readonly target = GeoPointSubject.createFromGeoPoint(new GeoPoint(0, 0)); + public readonly target = GeoPointSubject.create(new GeoPoint(0, 0)); } \ No newline at end of file diff --git a/src/garminsdk/components/map/modules/MapTrackVectorModule.ts b/src/garminsdk/components/map/modules/MapTrackVectorModule.ts index 2f50b14ac..206e1a5b9 100644 --- a/src/garminsdk/components/map/modules/MapTrackVectorModule.ts +++ b/src/garminsdk/components/map/modules/MapTrackVectorModule.ts @@ -8,5 +8,5 @@ export class MapTrackVectorModule { public readonly show = Subject.create(false); /** The track vector's lookahead time. */ - public readonly lookaheadTime = NumberUnitSubject.createFromNumberUnit(UnitType.SECOND.createNumber(60)); + public readonly lookaheadTime = NumberUnitSubject.create(UnitType.SECOND.createNumber(60)); } \ No newline at end of file diff --git a/src/garminsdk/components/map/modules/MapWaypointsModule.ts b/src/garminsdk/components/map/modules/MapWaypointsModule.ts index 6a0a2d96c..769a78142 100644 --- a/src/garminsdk/components/map/modules/MapWaypointsModule.ts +++ b/src/garminsdk/components/map/modules/MapWaypointsModule.ts @@ -25,9 +25,12 @@ export class MapWaypointsModule { /** Whether to show user waypoints. */ public readonly userShow = Subject.create(true); - /** Whether to show runway outlines and labels. */ + /** Whether to show runway outlines. */ public readonly runwayShow = Subject.create(true); + /** Whether to show runway labels. */ + public readonly runwayLabelShow = Subject.create(true); + /** The minimum projected length of a runway, in pixels, required to show its label. */ public readonly runwayLabelMinLength = Subject.create(50); } \ No newline at end of file diff --git a/src/garminsdk/components/map/modules/index.ts b/src/garminsdk/components/map/modules/index.ts index 854b3d0b7..8072587b6 100644 --- a/src/garminsdk/components/map/modules/index.ts +++ b/src/garminsdk/components/map/modules/index.ts @@ -7,6 +7,7 @@ export * from './MapGarminDataIntegrityModule'; export * from './MapGarminTrafficModule'; export * from './MapNexradModule'; export * from './MapOrientationModule'; +export * from './MapPanningModule'; export * from './MapPointerModule'; export * from './MapProcedurePreviewModule'; export * from './MapRangeCompassModule'; diff --git a/src/garminsdk/components/navdatabar/DefaultNavDataBarFieldModelFactory.ts b/src/garminsdk/components/navdatabar/DefaultNavDataBarFieldModelFactory.ts index 19d9427d6..1554e5289 100644 --- a/src/garminsdk/components/navdatabar/DefaultNavDataBarFieldModelFactory.ts +++ b/src/garminsdk/components/navdatabar/DefaultNavDataBarFieldModelFactory.ts @@ -3,20 +3,21 @@ import { EventBus, Subscribable } from '@microsoft/msfs-sdk'; import { Fms } from '../../flightplan/Fms'; import { NavDataFieldGpsValidity } from '../navdatafield/NavDataFieldModel'; import { NavDataFieldType } from '../navdatafield/NavDataFieldType'; -import { - GenericNavDataBarFieldModelFactory, NavDataBarFieldBrgModelFactory, NavDataBarFieldDestModelFactory, NavDataBarFieldDisModelFactory, - NavDataBarFieldDtgModelFactory, NavDataBarFieldDtkModelFactory, NavDataBarFieldEndModelFactory, NavDataBarFieldEnrModelFactory, NavDataBarFieldEtaModelFactory, - NavDataBarFieldEteModelFactory, NavDataBarFieldFobModelFactory, NavDataBarFieldFodModelFactory, NavDataBarFieldGsModelFactory, NavDataBarFieldIsaModelFactory, - NavDataBarFieldLdgModelFactory, NavDataBarFieldTasModelFactory, NavDataBarFieldTkeModelFactory, NavDataBarFieldTrkModelFactory, - NavDataBarFieldVsrModelFactory, NavDataBarFieldWptModelFactory, NavDataBarFieldXtkModelFactory -} from './GenericNavDataBarFieldModelFactory'; +import { GenericNavDataBarFieldModelFactory } from './GenericNavDataBarFieldModelFactory'; import { NavDataBarFieldModelFactory, NavDataBarFieldTypeModelMap } from './NavDataBarFieldModel'; +import { + NavDataBarFieldBrgModelFactory, NavDataBarFieldDestModelFactory, NavDataBarFieldDisModelFactory, NavDataBarFieldDtgModelFactory, + NavDataBarFieldDtkModelFactory, NavDataBarFieldEndModelFactory, NavDataBarFieldEnrModelFactory, NavDataBarFieldEtaModelFactory, + NavDataBarFieldEteModelFactory, NavDataBarFieldFobModelFactory, NavDataBarFieldFodModelFactory, NavDataBarFieldGsModelFactory, + NavDataBarFieldIsaModelFactory, NavDataBarFieldLdgModelFactory, NavDataBarFieldTasModelFactory, NavDataBarFieldTkeModelFactory, + NavDataBarFieldTrkModelFactory, NavDataBarFieldVsrModelFactory, NavDataBarFieldWptModelFactory, NavDataBarFieldXtkModelFactory +} from './NavDataBarFieldTypeModelFactories'; /** * A default implementation of NavDataBarFieldModelFactory. */ export class DefaultNavDataBarFieldModelFactory implements NavDataBarFieldModelFactory { - private readonly factory: GenericNavDataBarFieldModelFactory; + protected readonly factory: GenericNavDataBarFieldModelFactory; /** * Constructor. diff --git a/src/garminsdk/components/navdatabar/EventBusNavDataBarFieldTypeModelFactory.ts b/src/garminsdk/components/navdatabar/EventBusNavDataBarFieldTypeModelFactory.ts new file mode 100644 index 000000000..e1180926d --- /dev/null +++ b/src/garminsdk/components/navdatabar/EventBusNavDataBarFieldTypeModelFactory.ts @@ -0,0 +1,23 @@ +import { EventBus, Subscribable } from '@microsoft/msfs-sdk'; + +import { NavDataFieldGpsValidity } from '../navdatafield/NavDataFieldModel'; +import { NavDataFieldType } from '../navdatafield/NavDataFieldType'; +import { NavDataBarFieldTypeModelFactory, NavDataBarFieldTypeModelMap } from './NavDataBarFieldModel'; + +/** + * An abstract implementation of {@link NavDataBarFieldTypeModelFactory} which accesses data from the event bus to use + * to create its data models. + */ +export abstract class EventBusNavDataBarFieldTypeModelFactory implements NavDataBarFieldTypeModelFactory { + protected readonly sub = this.bus.getSubscriber(); + + /** + * Constructor. + * @param bus The event bus. + */ + constructor(private readonly bus: EventBus) { + } + + /** @inheritdoc */ + public abstract create(gpsValidity: Subscribable): NavDataBarFieldTypeModelMap[T]; +} \ No newline at end of file diff --git a/src/garminsdk/components/navdatabar/GenericNavDataBarFieldModelFactory.ts b/src/garminsdk/components/navdatabar/GenericNavDataBarFieldModelFactory.ts index c25cd779f..703cbf963 100644 --- a/src/garminsdk/components/navdatabar/GenericNavDataBarFieldModelFactory.ts +++ b/src/garminsdk/components/navdatabar/GenericNavDataBarFieldModelFactory.ts @@ -1,29 +1,8 @@ -import { - AdcEvents, AhrsEvents, BasicNavAngleSubject, BasicNavAngleUnit, ClockEvents, EngineEvents, EventBus, FlightPlanCopiedEvent, - FlightPlanIndicationEvent, FlightPlannerEvents, FlightPlanOriginDestEvent, GNSSEvents, ICAO, LNavEvents, NavAngleUnitFamily, NavMath, NumberUnitInterface, - NumberUnitSubject, OriginDestChangeType, Subject, Subscribable, UnitFamily, UnitType, VNavDataEvents, VNavEvents -} from '@microsoft/msfs-sdk'; +import { Subscribable } from '@microsoft/msfs-sdk'; -import { Fms } from '../../flightplan/Fms'; -import { LNavDataEvents } from '../../navigation/LNavDataEvents'; import { NavDataFieldGpsValidity } from '../navdatafield/NavDataFieldModel'; import { NavDataFieldType } from '../navdatafield/NavDataFieldType'; -import { - NavDataBarFieldConsumerValueModel, NavDataBarFieldConsumerValueNumberUnitModel, NavDataBarFieldGenericModel, NavDataBarFieldModel, NavDataBarFieldModelFactory, - NavDataBarFieldTypeModelMap -} from './NavDataBarFieldModel'; - -/** - * A factory which creates data models for a single type of navigation data bar field. - */ -export interface NavDataBarFieldTypeModelFactory { - /** - * Creates a navigation data bar field data model for this factory's data field type. - * @param gpsValidity The subscribable that provides the validity of the GPS data for the models. - * @returns A navigation data bar field data model for this factory's data field type. - */ - create(gpsValidity: Subscribable): NavDataBarFieldTypeModelMap[T]; -} +import { NavDataBarFieldModelFactory, NavDataBarFieldTypeModelFactory, NavDataBarFieldTypeModelMap } from './NavDataBarFieldModel'; /** * A generic implementation of a factory for navigation data bar field data models. For each data field type, a @@ -72,557 +51,4 @@ export class GenericNavDataBarFieldModelFactory implements NavDataBarFieldModelF return model as NavDataBarFieldTypeModelMap[T]; } -} - -/** - * An abstract implementation of {@link NavDataBarFieldTypeModelFactory} which accesses data from the event bus to use - * to create its data models. - */ -export abstract class EventBusNavDataBarFieldTypeModelFactory implements NavDataBarFieldTypeModelFactory { - protected readonly sub = this.bus.getSubscriber(); - - /** - * Constructor. - * @param bus The event bus. - */ - constructor(private readonly bus: EventBus) { - } - - /** @inheritdoc */ - public abstract create(gpsValidity: Subscribable): NavDataBarFieldTypeModelMap[T]; -} - -/** - * Creates data models for Bearing to Waypoint navigation data bar fields. - */ -export class NavDataBarFieldBrgModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - BasicNavAngleSubject.create(BasicNavAngleUnit.create(true).createNumber(0)), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_waypoint_bearing_mag').whenChanged(), - this.sub.on('magvar') - ], - [false, 0, 0] as [boolean, number, number], - (sub, validity, [isTracking, bearing, magVar]) => { - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - sub.set((isTracking.get() && gpsValid) ? bearing.get() : NaN, magVar.get()); - } - ); - } -} - -/** - * Creates data models for Bearing to Waypoint navigation data bar fields. - */ -export class NavDataBarFieldDestModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** - * Constructor. - * @param bus The event bus. - * @param fms The flight management system. - */ - constructor(bus: EventBus, private readonly fms: Fms) { - super(bus); - } - - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel { - let destinationIdent = '____'; - - const originDestHandler = (event: FlightPlanOriginDestEvent): void => { - if (event.planIndex === Fms.PRIMARY_PLAN_INDEX && event.type === OriginDestChangeType.DestinationAdded) { - destinationIdent = event.airport === undefined ? '____' : ICAO.getIdent(event.airport); - } else if (event.type === OriginDestChangeType.DestinationRemoved) { - destinationIdent = '____'; - } - }; - const loadHandler = (event: FlightPlanIndicationEvent): void => { - if (event.planIndex !== Fms.PRIMARY_PLAN_INDEX) { - return; - } - - const primaryPlan = this.fms.getPrimaryFlightPlan(); - destinationIdent = primaryPlan.destinationAirport === undefined ? '____' : ICAO.getIdent(primaryPlan.destinationAirport); - }; - const copyHandler = (event: FlightPlanCopiedEvent): void => { - if (event.targetPlanIndex !== Fms.PRIMARY_PLAN_INDEX) { - return; - } - - const primaryPlan = this.fms.getPrimaryFlightPlan(); - destinationIdent = primaryPlan.destinationAirport === undefined ? '____' : ICAO.getIdent(primaryPlan.destinationAirport); - }; - - const originDestConsumer = this.sub.on('fplOriginDestChanged'); - originDestConsumer.handle(originDestHandler); - - const loadConsumer = this.sub.on('fplLoaded'); - loadConsumer.handle(loadHandler); - - const copyConsumer = this.sub.on('fplCopied'); - copyConsumer.handle(copyHandler); - - return new NavDataBarFieldGenericModel( - Subject.create('____'), - gpsValidity, - (sub) => { - sub.set(destinationIdent); - }, - () => { - originDestConsumer.off(originDestHandler); - loadConsumer.off(loadHandler); - copyConsumer.off(copyHandler); - } - ); - } -} - -/** - * Creates data models for Distance to Waypoint navigation data bar fields. - */ -export class NavDataBarFieldDisModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.NMILE.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_waypoint_distance').whenChanged() - ], - [false, 0] as [boolean, number], - (sub, validity, [isTracking, distance]) => { - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - sub.set((isTracking.get() && gpsValid) ? distance.get() : NaN); - } - ); - } -} - -/** - * Creates data models for Distance to Destination navigation data bar fields. - */ -export class NavDataBarFieldDtgModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.NMILE.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_destination_distance').whenChanged() - ], - [false, 0] as [boolean, number], - (sub, validity, [isTracking, distance]) => { - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - sub.set((isTracking.get() && gpsValid) ? distance.get() : NaN); - } - ); - } -} - -/** - * Creates data models for Desired Track navigation data bar fields. - */ -export class NavDataBarFieldDtkModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - BasicNavAngleSubject.create(BasicNavAngleUnit.create(true).createNumber(0)), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_dtk_mag').whenChanged(), - this.sub.on('magvar') - ], - [false, 0, 0] as [boolean, number, number], - (sub, validity, [isTracking, track, magVar]) => { - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - sub.set((isTracking.get() && gpsValid) ? track.get() : NaN, magVar.get()); - } - ); - } -} - -/** - * Creates data models for Endurance navigation data bar fields. - */ -export class NavDataBarFieldEndModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.HOUR.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('fuel_usable_total').whenChanged(), - this.sub.on('fuel_flow_total').whenChanged() - ], - [0, 0] as [number, number], - (sub, validity, [fuelRemaining, fuelFlow]) => { - let endurance = NaN; - const fuelFlowGph = fuelFlow.get(); - if (fuelFlowGph > 0) { - const fuelGal = fuelRemaining.get(); - endurance = fuelGal / fuelFlowGph; - } - sub.set(endurance); - } - ); - } -} - -/** - * Creates data models for Time To Destination navigation data bar fields. - */ -export class NavDataBarFieldEnrModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.HOUR.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_destination_distance').whenChanged(), - this.sub.on('ground_speed').whenChanged() - ], - [false, 0, 0] as [boolean, number, number], - (sub, validity, [isTracking, distance, gs]) => { - let time = NaN; - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - - if (isTracking.get() && gpsValid) { - const gsKnots = gs.get(); - if (gsKnots > 30) { - const distanceNM = distance.get(); - time = distanceNM / gsKnots; - } - } - sub.set(time); - } - ); - } -} - -/** - * Creates data models for Estimated Time of Arrival navigation data bar fields. - */ -export class NavDataBarFieldEtaModelFactory - extends EventBusNavDataBarFieldTypeModelFactory { - - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel { - return new NavDataBarFieldConsumerValueModel( - Subject.create(NaN), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_waypoint_distance').whenChanged(), - this.sub.on('ground_speed').whenChanged(), - this.sub.on('simTime') - ], - [false, 0, 0, NaN] as [boolean, number, number, number], - (sub, validity, [isTracking, distance, gs, time]) => { - let eta = NaN; - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - - if (isTracking.get() && gpsValid) { - const gsKnots = gs.get(); - if (gsKnots > 30) { - const distanceNM = distance.get(); - eta = UnitType.HOUR.convertTo(distanceNM / gsKnots, UnitType.MILLISECOND) + time.get(); - } - } - sub.set(eta); - } - ); - } -} - -/** - * Creates data models for Time To Waypoint navigation data bar fields. - */ -export class NavDataBarFieldEteModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.HOUR.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_waypoint_distance').whenChanged(), - this.sub.on('ground_speed').whenChanged() - ], - [false, 0, 0] as [boolean, number, number], - (sub, validity, [isTracking, distance, gs]) => { - let time = NaN; - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - - if (isTracking.get() && gpsValid) { - const gsKnots = gs.get(); - if (gsKnots > 30) { - const distanceNM = distance.get(); - time = distanceNM / gsKnots; - } - } - sub.set(time); - } - ); - } -} - -/** - * Creates data models for Fuel on Board navigation data bar fields. - */ -export class NavDataBarFieldFobModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueNumberUnitModel( - gpsValidity, - this.sub.on('fuel_usable_total'), 0, UnitType.GALLON_FUEL - ); - } -} - -/** - * Creates data models for Fuel Over Destination navigation data bar fields. - */ -export class NavDataBarFieldFodModelFactory - extends EventBusNavDataBarFieldTypeModelFactory { - - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.GALLON_FUEL.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_destination_distance').whenChanged(), - this.sub.on('ground_speed').whenChanged(), - this.sub.on('fuel_usable_total').whenChanged(), - this.sub.on('fuel_flow_total').whenChanged() - ], - [false, 0, 0, 0, 0] as [boolean, number, number, number, number], - (sub, validity, [isTracking, distance, gs, fuelRemaining, fuelFlow]) => { - let fod = NaN; - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - - if (isTracking.get() && gpsValid) { - const gsKnots = gs.get(); - const fuelFlowGph = fuelFlow.get(); - if (gsKnots > 30 && fuelFlowGph > 0) { - const distanceNM = distance.get(); - const fuelGal = fuelRemaining.get(); - fod = fuelGal - distanceNM / gsKnots * fuelFlowGph; - } - } - sub.set(fod); - } - ); - } -} - -/** - * Creates data models for Ground Speed navigation data bar fields. - */ -export class NavDataBarFieldGsModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.KNOT.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('ground_speed') - ], - [0], - (sub, validity, [gs]) => { - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - sub.set(gpsValid ? gs.get() : NaN); - } - ); - } -} - -/** - * Creates data models for ISA navigation data bar fields. - */ -export class NavDataBarFieldIsaModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.DELTA_CELSIUS.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('ambient_temp_c'), - this.sub.on('isa_temp_c') - ], - [0], - (sub, validity, [sat, isa]) => { - sub.set(sat.get() - isa.get()); - } - ); - } -} - -/** - * Creates data models for Estimated Time of Arrival at Destination navigation data bar fields. - */ -export class NavDataBarFieldLdgModelFactory - extends EventBusNavDataBarFieldTypeModelFactory { - - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel { - return new NavDataBarFieldConsumerValueModel( - Subject.create(NaN), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_destination_distance').whenChanged(), - this.sub.on('ground_speed').whenChanged(), - this.sub.on('simTime') - ], - [false, 0, 0, NaN] as [boolean, number, number, number], - (sub, validity, [isTracking, distance, gs, time]) => { - let eta = NaN; - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - - if (isTracking.get() && gpsValid) { - const gsKnots = gs.get(); - if (gsKnots > 30) { - const distanceNM = distance.get(); - eta = UnitType.HOUR.convertTo(distanceNM / gsKnots, UnitType.MILLISECOND) + time.get(); - } - } - sub.set(eta); - } - ); - } -} - -/** - * Creates data models for True Airspeed navigation data bar fields. - */ -export class NavDataBarFieldTasModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueNumberUnitModel( - gpsValidity, - this.sub.on('tas'), 0, UnitType.KNOT - ); - } -} - -/** - * Creates data models for Track Angle Error navigation data bar fields. - */ -export class NavDataBarFieldTkeModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.DEGREE.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_dtk_true').whenChanged(), - this.sub.on('track_deg_true').whenChanged() - ], - [false, 0, 0] as [boolean, number, number], - (sub, validity, [isTracking, dtk, track]) => { - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - sub.set((isTracking.get() && gpsValid) ? NavMath.diffAngle(dtk.get(), track.get()) : NaN); - } - ); - } -} - -/** - * Creates data models for Ground Track navigation data bar fields. - */ -export class NavDataBarFieldTrkModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - BasicNavAngleSubject.create(BasicNavAngleUnit.create(true).createNumber(0)), - gpsValidity, - [ - this.sub.on('hdg_deg_true'), - this.sub.on('ground_speed'), - this.sub.on('track_deg_magnetic'), - this.sub.on('magvar') - ], - [0, 0, 0, 0] as [number, number, number, number], - (sub, validity, [hdg, gs, track, magVar]) => { - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - - if (gs.get() < 5) { - sub.set(gpsValid ? hdg.get() : NaN, magVar.get()); - } else { - sub.set(gpsValid ? track.get() : NaN, magVar.get()); - } - } - ); - } -} - -/** - * Creates data models for Vertical Speed Required navigation data bar fields. - */ -export class NavDataBarFieldVsrModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.FPM.createNumber(NaN)), - gpsValidity, - [this.sub.on('vnav_required_vs')], - [0], - (sub, validity, [vsrSub]) => { - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - const vsr = vsrSub.get(); - sub.set((gpsValid && vsr !== 0) ? vsr : NaN); - } - ); - } -} - -/** - * Creates data models for Active Wpt navigation data bar fields. - */ -export class NavDataBarFieldWptModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritDoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel { - return new NavDataBarFieldConsumerValueModel( - Subject.create('_____'), - gpsValidity, - [this.sub.on('lnavdata_waypoint_ident')], - [''], - (sub, validity, [identSubject]) => { - const ident = identSubject.get(); - sub.set(ident === '' ? '_____' : ident); - } - ); - } -} - -/** - * Creates data models for Cross Track navigation data bar fields. - */ -export class NavDataBarFieldXtkModelFactory extends EventBusNavDataBarFieldTypeModelFactory { - /** @inheritdoc */ - public create(gpsValidity: Subscribable): NavDataBarFieldModel> { - return new NavDataBarFieldConsumerValueModel( - NumberUnitSubject.create(UnitType.NMILE.createNumber(NaN)), - gpsValidity, - [ - this.sub.on('lnav_is_tracking').whenChanged(), - this.sub.on('lnavdata_xtk').whenChanged() - ], - [false, 0] as [boolean, number], - (sub, validity, [isTracking, xtk]) => { - const gpsValid = validity.get() === NavDataFieldGpsValidity.DeadReckoning || validity.get() === NavDataFieldGpsValidity.Valid; - sub.set((isTracking.get() && gpsValid) ? xtk.get() : NaN); - } - ); - } } \ No newline at end of file diff --git a/src/garminsdk/components/navdatabar/NavDataBar.tsx b/src/garminsdk/components/navdatabar/NavDataBar.tsx index c4fe4a48e..cbee41aba 100644 --- a/src/garminsdk/components/navdatabar/NavDataBar.tsx +++ b/src/garminsdk/components/navdatabar/NavDataBar.tsx @@ -41,7 +41,7 @@ export class NavDataBar extends DisplayComponent { private static readonly RESERVED_CSS_CLASSES = ['nav-data-bar']; private readonly fieldCount = Math.max(0, this.props.fieldCount); - private readonly fieldSlots: VNode[] = Array.from({ length: this.fieldCount }, () =>
); + private readonly fieldSlots: VNode[] = Array.from({ length: this.fieldCount }, () => ); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDPageSelect.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDPageSelect.tsx index e07e1d4a4..2f9e4d715 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDPageSelect.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDPageSelect.tsx @@ -1,4 +1,4 @@ -import { ArraySubject, FSComponent, NodeReference, PluginSystem, VNode } from '@microsoft/msfs-sdk'; +import { ArraySubject, FSComponent, HEvent, NodeReference, PluginSystem, VNode } from '@microsoft/msfs-sdk'; import { G1000AvionicsPlugin, G1000PluginBinder } from '../../../Shared'; import { MenuItemDefinition, PopoutMenuItem } from '../../../Shared/UI/Dialogs/PopoutMenuItem'; @@ -210,7 +210,7 @@ export class MFDPageSelect extends UiView { this.compute(); // Now plugins can modify things. - this.props.pluginSystem.callPlugins(p => p.onPageSelectMenuSystemInitialized()); + this.props.pluginSystem.callPlugins(p => p.onPageSelectMenuSystemInitialized?.()); } // eslint-disable-next-line jsdoc/require-jsdoc @@ -228,6 +228,11 @@ export class MFDPageSelect extends UiView { this.setActiveGroup(groupIndex, itemIndex); } }, true); + + // Scroll to Navigation Map (first list item) in pag elist when home button is pressed + this.props.pluginSystem.binder?.bus.getSubscriber().on('hEvent').handle(eventName => { + if (eventName === 'AS1000_CONTROL_PAD_Home') { this.listRef.instance.scrollToIndex(0); } + }); } /** diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDUiPage.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDUiPage.tsx index a6fa4fd82..7b76a653b 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDUiPage.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDUiPage.tsx @@ -59,6 +59,8 @@ export abstract class MFDUiPage exten return this.onDirectToPressed(); case FmsHEvent.MENU: return this.onMenuPressed(); + case FmsHEvent.HOME: + return this.onHomePressed(); } return false; @@ -72,6 +74,15 @@ export abstract class MFDUiPage exten return false; } + /** + * This method is called when a HOME button event occurs. + * @returns whether the event was handled. + */ + protected onHomePressed(): boolean { + this.props.viewService.open('NavMapPage'); + return true; + } + /** * This method is called when a PROC button event occurs. * @returns whether the event was handled. diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDViewService.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDViewService.ts index 7126ea321..d8d9d8f60 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDViewService.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/MFDViewService.ts @@ -1,3 +1,6 @@ +import { EventBus, Publisher } from '@microsoft/msfs-sdk'; + +import { MFDViewServiceEvents } from '../../../Shared/UI/Controllers/ControlpadInputController'; import { FmsHEvent } from '../../../Shared/UI/FmsHEvent'; import { ViewService } from '../../../Shared/UI/ViewService'; @@ -6,6 +9,9 @@ import { ViewService } from '../../../Shared/UI/ViewService'; */ export class MFDViewService extends ViewService { + /** A publisher for publishing flight planner update events. */ + private readonly publisher: Publisher; + protected readonly fmsEventMap: Map = new Map([ ['AS1000_MFD_FMS_Upper_INC', FmsHEvent.UPPER_INC], ['AS1000_MFD_FMS_Upper_DEC', FmsHEvent.UPPER_DEC], @@ -24,6 +30,22 @@ export class MFDViewService extends ViewService { ['AS1000_MFD_JOYSTICK_LEFT', FmsHEvent.JOYSTICK_LEFT], ['AS1000_MFD_JOYSTICK_UP', FmsHEvent.JOYSTICK_UP], ['AS1000_MFD_JOYSTICK_RIGHT', FmsHEvent.JOYSTICK_RIGHT], - ['AS1000_MFD_JOYSTICK_DOWN', FmsHEvent.JOYSTICK_DOWN] + ['AS1000_MFD_JOYSTICK_DOWN', FmsHEvent.JOYSTICK_DOWN], ]); -} \ No newline at end of file + + /** + * Constructs the view service. + * @param bus The event bus. + */ + constructor(readonly bus: EventBus) { + super(bus); + + this.publisher = bus.getPublisher(); + + // For the controlpad input routing, we need to share among the instruments, whether the MFD has opened the home view (nav map) or not: + this.activeViewKey.sub(newViewKey => { + const isNotNavMapPage = newViewKey !== 'NavMapPage'; // Prevent generic controlpad use (COM, NAV, XPDR) if any otehr page than NavMap is opened. + this.bus.getPublisher().pub('inhibitGenericControlpadUse', isNotNavMapPage, true, true); + }, true); + } +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/NavDataBar/MFDNavDataBar.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/NavDataBar/MFDNavDataBar.css index 01c045af8..23a536162 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/NavDataBar/MFDNavDataBar.css +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/NavDataBar/MFDNavDataBar.css @@ -1,33 +1,35 @@ .nav-data-bar-container { position: relative; - background: linear-gradient(to bottom, rgb(4,4,12), rgb(24, 28, 43)); + background: linear-gradient(to bottom, rgb(4, 4, 12), rgb(24, 28, 43)); border-style: solid; border-width: 1px; - border-color:rgb(133, 133, 133); + border-color: rgb(133, 133, 133); border-top: none; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; } - .nav-data-bar { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 50%; - padding: 0 2px; - display: grid; - grid-template-rows: 100%; - grid-template-columns: repeat(auto-fit, minmax(10px, 1fr)); - align-items: center; - font-size: 20px; - } - .nav-data-bar-page-title { - position: absolute; - left: 0; - top: 75%; - transform: translateY(-50%); - width: 100%; - text-align: center; - font-size: 20px; - color: cyan; - } \ No newline at end of file + +.nav-data-bar { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 50%; + padding: 0 2px; + display: grid; + grid-template-rows: 100%; + grid-template-columns: repeat(auto-fit, minmax(10px, 1fr)); + align-items: center; + font-size: 20px; +} + +.nav-data-bar-page-title { + position: absolute; + left: 0; + top: 75%; + transform: translateY(-50%); + width: 100%; + text-align: center; + font-size: 20px; + color: cyan; +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Nearest/Airports/FrequenciesGroup.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Nearest/Airports/FrequenciesGroup.tsx index 2bcbb7341..3952c3335 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Nearest/Airports/FrequenciesGroup.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Nearest/Airports/FrequenciesGroup.tsx @@ -74,7 +74,7 @@ export class NearestAirportFrequenciesGroup extends G1000UiControl { +export class FrequencyItem extends G1000UiControl { private readonly number = FSComponent.createRef(); /** @inheritdoc */ @@ -158,4 +158,4 @@ class FrequencyItem extends G1000UiControl {
); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Nearest/MFDNearestAirportsPage.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Nearest/MFDNearestAirportsPage.tsx index ec07c3856..607861b0f 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Nearest/MFDNearestAirportsPage.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Nearest/MFDNearestAirportsPage.tsx @@ -1,7 +1,7 @@ import { AdaptiveNearestSubscription, AirportClassMask, AirportFacility, AirportUtils, ApproachProcedure, FocusPosition, FSComponent, - NearestAirportSubscription, NearestSubscription, VNode + NearestAirportSubscription, NearestSubscription, Subject, Subscription, VNode } from '@microsoft/msfs-sdk'; import { ProcedureType } from '@microsoft/msfs-garminsdk'; @@ -15,6 +15,8 @@ import { NearestAirportInformationGroup, NearestAirportRunwaysGroup, } from './Airports'; import { MFDNearestPage, MFDNearestPageProps } from './MFDNearestPage'; +import { MFDPageMenuDialog } from '../MFDPageMenuDialog'; +import { MenuItemDefinition } from '../../../../Shared'; export enum NearestAirportSoftKey { APT, @@ -30,17 +32,70 @@ export enum NearestAirportSoftKey { */ export class MFDNearestAirportsPage extends MFDNearestPage { + private g1000ControlPublisher = this.props.bus.getPublisher(); + private readonly informationGroup = FSComponent.createRef(); private readonly runwaysGroup = FSComponent.createRef(); private readonly frequenciesGroup = FSComponent.createRef(); private readonly approachesGroup = FSComponent.createRef(); - private facility: AirportFacility | undefined; - private approach: ApproachProcedure | undefined; + private pageMenu?: MFDPageMenuDialog; + private pageMenuSub?: Subscription; + private isPageMenuRunwayEnabledSub?: Subscription; + private readonly isPageMenuRunwayEnabled = Subject.create(false); + + private facility: AirportFacility | undefined = undefined; + private approach: ApproachProcedure | undefined = undefined; private searchSettings = NearestAirportSearchSettings.getManager(this.props.bus); private searchSubscription: NearestAirportSubscription | undefined; + private readonly pageMenuItems = (): MenuItemDefinition[] => [ + { + id: 'select-airport-window', + renderContent: (): VNode => Select Airport Window, + action: (): void => { + this.g1000ControlPublisher.pub('nearest_airports_key', NearestAirportSoftKey.APT); + } + }, + { + id: 'select-runway-window', + renderContent: (): VNode => Select Runway Window, + isEnabled: this.isPageMenuRunwayEnabled.get(), + action: (): void => { + this.g1000ControlPublisher.pub('nearest_airports_key', NearestAirportSoftKey.RNWY); + } + }, + { + id: 'select-frequency-window', + renderContent: (): VNode => Select Frequency Window, + action: (): void => { + this.g1000ControlPublisher.pub('nearest_airports_key', NearestAirportSoftKey.FREQ); + } + }, + { + id: 'select-approach-window', + renderContent: (): VNode => Select Approach Window, + action: (): void => { + this.g1000ControlPublisher.pub('nearest_airports_key', NearestAirportSoftKey.APR); + } + }, + { + id: 'load-approach', + renderContent: (): VNode => Load Approach, + isEnabled: this.pageMenu?.inputData.get() !== undefined && this.approach !== undefined, + action: (): void => { + this.g1000ControlPublisher.pub('nearest_airports_key', NearestAirportSoftKey.LD_APR); + } + }, + { + id: 'charts', + renderContent: (): VNode => Charts, + isEnabled: false, + }, + ]; + + /** * Creates an instance of a nearest airport box. * @param props The props. @@ -94,6 +149,36 @@ export class MFDNearestAirportsPage extends MFDNearestPage { this.props.menuSystem.clear(); this.props.menuSystem.pushMenu('nearest-airports-menu'); + this._title.set('NRST - Nearest Airports'); + } + + /** + * Opens the Page Menu popup when Menu button is pressed. + * @returns whether the event was handled. + */ + protected override onMenuPressed(): boolean { + if (this.pageMenuSub) { + this.pageMenuSub.destroy(); + this.pageMenuSub = undefined; + } + + if (this.isPageMenuRunwayEnabledSub) { + this.isPageMenuRunwayEnabledSub.destroy(); + this.isPageMenuRunwayEnabledSub = undefined; + } + + this.pageMenu = this.props.viewService.open('PageMenuDialog'); + this.pageMenuSub = this.pageMenu + .setInput(this.facility) + .inputData.sub((input: AirportFacility | undefined) => { + this.isPageMenuRunwayEnabled.set(input !== undefined && input.runways.length > 1); + }, true); + + this.isPageMenuRunwayEnabled.sub(() => { + this.pageMenu?.setMenuItems(this.pageMenuItems()); + }, true); + + return true; } /** @inheritdoc */ @@ -120,7 +205,6 @@ export class MFDNearestAirportsPage extends MFDNearestPage { /** @inheritdoc */ protected onFacilitySelected(airport: AirportFacility | null): void { super.onFacilitySelected(airport); - this.informationGroup.instance.set(airport); this.runwaysGroup.instance.set(airport); this.frequenciesGroup.instance.set(airport); @@ -148,12 +232,11 @@ export class MFDNearestAirportsPage extends MFDNearestPage { */ private onApproachSelected(approach: ApproachProcedure | undefined): void { this.approach = approach; - const publisher = this.props.bus.getPublisher(); if (this.approach !== undefined) { - publisher.pub('ld_apr_enabled', true, false, false); + this.g1000ControlPublisher.pub('ld_apr_enabled', true, false, false); } else { - publisher.pub('ld_apr_enabled', false, false, false); + this.g1000ControlPublisher.pub('ld_apr_enabled', false, false, false); } } @@ -177,4 +260,22 @@ export class MFDNearestAirportsPage extends MFDNearestPage { ); } + + /** @inheritdoc */ + public pause(): void { + this.pageMenuSub?.pause(); + this.isPageMenuRunwayEnabledSub?.pause(); + } + + /** @inheritdoc */ + public resume(): void { + this.pageMenuSub?.resume(); + this.isPageMenuRunwayEnabledSub?.resume(); + } + + /** @inheritdoc */ + public destroy(): void { + this.pageMenuSub?.destroy(); + this.isPageMenuRunwayEnabledSub?.destroy(); + } } diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Procedure/MFDProc.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Procedure/MFDProc.tsx index 80c16275f..211856c55 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Procedure/MFDProc.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Procedure/MFDProc.tsx @@ -158,6 +158,7 @@ export class MFDProc extends UiView { switch (evt) { case FmsHEvent.PROC: case FmsHEvent.CLR: + case FmsHEvent.HOME: this.close(); return true; } diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Procedure/ProcSequenceItem.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Procedure/ProcSequenceItem.tsx index 6b3ff5e21..0ab64d6a6 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Procedure/ProcSequenceItem.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/Procedure/ProcSequenceItem.tsx @@ -61,7 +61,7 @@ export class ProcSequenceItem extends UiControl { oldVal.set(newVal); }, BasicNavAngleUnit.create(true).createNumber(NaN) - ) as MappedSubscribable>; + ) as MappedSubscribable>; private readonly distanceSub = this.props.data.map( leg => { diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupDataBarGroup.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupDataBarGroup.tsx index 4174a62d8..eb54d09fb 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupDataBarGroup.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupDataBarGroup.tsx @@ -11,11 +11,33 @@ import { MFDSystemSetupSelectRow } from './MFDSystemSetupRow'; * The MFD System Setup page MFD Data Bar Fields group. */ export class MFDSystemSetupDataBarGroup extends DisplayComponent { + private static readonly SUPPORTED_FIELD_TYPES = [ + NavDataFieldType.BearingToWaypoint, + NavDataFieldType.Destination, + NavDataFieldType.DistanceToWaypoint, + NavDataFieldType.DistanceToDestination, + NavDataFieldType.DesiredTrack, + NavDataFieldType.Endurance, + NavDataFieldType.TimeToDestination, + NavDataFieldType.TimeOfWaypointArrival, + NavDataFieldType.TimeToWaypoint, + NavDataFieldType.FuelOnBoard, + NavDataFieldType.FuelOverDestination, + NavDataFieldType.GroundSpeed, + NavDataFieldType.ISA, + NavDataFieldType.TimeOfDestinationArrival, + NavDataFieldType.TrueAirspeed, + NavDataFieldType.TrackAngleError, + NavDataFieldType.GroundTrack, + NavDataFieldType.VerticalSpeedRequired, + NavDataFieldType.CrossTrack + ]; + private readonly settingManager = MFDNavDataBarUserSettings.getManager(this.props.bus); /** @inheritdoc */ public render(): VNode { - const valueArray = ArraySubject.create(Object.values(NavDataFieldType).filter(type => type !== NavDataFieldType.TimeToDestination)); + const valueArray = ArraySubject.create(Array.from(MFDSystemSetupDataBarGroup.SUPPORTED_FIELD_TYPES)); return ( diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupDateTimeGroup.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupDateTimeGroup.tsx index 9ed538a9a..946a818c4 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupDateTimeGroup.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupDateTimeGroup.tsx @@ -8,7 +8,7 @@ import { TimeDisplay, TimeDisplayFormat } from '../../../../Shared/UI/Common/Tim import { UiControl } from '../../../../Shared/UI/UiControl'; import { DigitInput } from '../../../../Shared/UI/UiControls2/DigitInput'; import { G1000UiControlWrapper } from '../../../../Shared/UI/UiControls2/G1000UiControlWrapper'; -import { GenericNumberInput } from '../../../../Shared/UI/UiControls2/GenericNumberInput'; +import { TimeNumberInput } from '../../../../Shared/UI/UiControls2/TimeNumberInput'; import { SignInput } from '../../../../Shared/UI/UiControls2/SignInput'; import { UserSettingNumberController } from '../../../../Shared/UI/UserSettings/UserSettingNumberController'; import { GroupBox } from '../GroupBox'; @@ -169,7 +169,7 @@ class LocalOffsetInput extends DisplayComponent { private static readonly MIN_TO_MS = UnitType.MINUTE.convertTo(1, UnitType.MILLISECOND); private readonly rootRef = FSComponent.createRef(); - private readonly inputRef = FSComponent.createRef(); + private readonly inputRef = FSComponent.createRef(); private readonly controller = new UserSettingNumberController( this.props.settingManager, @@ -204,7 +204,7 @@ class LocalOffsetInput extends DisplayComponent { return (
- { @@ -231,10 +231,10 @@ class LocalOffsetInput extends DisplayComponent { : - +
––:––
); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupRow.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupRow.tsx index 5409b3541..206bd378e 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupRow.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/Components/UI/SystemSetup/MFDSystemSetupRow.tsx @@ -139,4 +139,4 @@ export class MFDSystemSetupToggleRow< /> ); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/WTG1000_MFD.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/WTG1000_MFD.tsx index 1a129b5b8..e075a1e0a 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/WTG1000_MFD.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/MFD/WTG1000_MFD.tsx @@ -6,16 +6,16 @@ /// import { - AdcPublisher, APRadioNavInstrument, AutopilotInstrument, AvionicsSystem, BaseInstrumentPublisher, Clock, ClockEvents, CompositeLogicXMLHost, ControlPublisher, EISPublisher, - ElectricalPublisher, EventBus, EventSubscriber, FacilityLoader, FacilityRepository, FlightPathAirplaneSpeedMode, FlightPathCalculator, FlightPlanner, - FSComponent, GameStateProvider, GNSSPublisher, GpsSynchronizer, HEvent, HEventPublisher, InstrumentBackplane, InstrumentEvents, LNavSimVarPublisher, MinimumsManager, - MinimumsSimVarPublisher, NavComSimVarPublisher, PluginSystem, SimVarValueType, SmoothingPathCalculator, TrafficInstrument, UserSettingSaveManager, - VNavSimVarPublisher, Wait, XMLGaugeConfigFactory + AdcPublisher, AltitudeSelectManagerAccelFilter, APRadioNavInstrument, AutopilotInstrument, AvionicsSystem, BaseInstrumentPublisher, Clock, ClockEvents, + CompositeLogicXMLHost, ControlPublisher, EISPublisher, ElectricalPublisher, EventBus, EventSubscriber, FacilityLoader, FacilityRepository, + FlightPathAirplaneSpeedMode, FlightPathCalculator, FlightPlanner, FSComponent, GameStateProvider, GNSSPublisher, GpsSynchronizer, HEvent, HEventPublisher, + InstrumentBackplane, InstrumentEvents, LNavSimVarPublisher, MinimumsManager, MinimumsSimVarPublisher, NavComSimVarPublisher, NavEvents, NavSourceType, + PluginSystem, SimVarValueType, SmoothingPathCalculator, TrafficInstrument, UserSettingSaveManager, VNavSimVarPublisher, VNode, Wait, XMLGaugeConfigFactory } from '@microsoft/msfs-sdk'; import { - AdcSystem, DateTimeUserSettings, Fms, GarminAdsb, GarminAPConfig, GarminAPStateManager, GarminVNavUtils, LNavDataSimVarPublisher, NavdataComputer, TrafficAdvisorySystem, - TrafficOperatingModeManager, UnitsUserSettings + ActiveNavSource, AdcSystem, DateTimeUserSettings, Fms, GarminAdsb, GarminAPConfig, GarminAPStateManager, GarminGoAroundManager, GarminNavEvents, + GarminVNavUtils, LNavDataSimVarPublisher, NavdataComputer, TrafficAdvisorySystem, TrafficOperatingModeManager, UnitsUserSettings } from '@microsoft/msfs-garminsdk'; import { PFDUserSettings } from '../PFD/PFDUserSettings'; @@ -35,6 +35,7 @@ import { StartupLogo } from '../Shared/StartupLogo'; import { ADCAvionicsSystem, AHRSSystem, AvionicsComputerSystem, EngineAirframeSystem, G1000AvionicsSystem, MagnetometerSystem, TransponderSystem } from '../Shared/Systems'; +import { ControlpadInputController, ControlpadTargetInstrument } from '../Shared/UI/Controllers/ControlpadInputController'; import { ContextMenuDialog } from '../Shared/UI/Dialogs/ContextMenuDialog'; import { MessageDialog } from '../Shared/UI/Dialogs/MessageDialog'; import { EngineMenu } from '../Shared/UI/Menus/MFD/EngineMenu'; @@ -160,6 +161,12 @@ class WTG1000_MFD extends BaseInstrument { private readonly pluginSystem = new PluginSystem(); + private readonly comRadio = FSComponent.createRef(); + private readonly navRadio = FSComponent.createRef(); + private controlPadHandler: ControlpadInputController; + + private goAroundManager?: GarminGoAroundManager; + /** * Creates an instance of the WTG1000_MFD. */ @@ -213,7 +220,7 @@ class WTG1000_MFD extends BaseInstrument { this.backplane.addPublisher('gnss', this.gnss); this.backplane.addPublisher('eis', this.eis); this.backplane.addPublisher('control', this.controlPublisher); - this.backplane.addPublisher('g1000', this.g1000ControlPublisher); + this.backplane.addPublisher('g1000Control', this.g1000ControlPublisher); this.backplane.addPublisher('lnav', this.lNavPublisher); this.backplane.addPublisher('lnavdata', this.lNavDataPublisher); this.backplane.addPublisher('vnav', this.vNavPublisher); @@ -268,6 +275,8 @@ class WTG1000_MFD extends BaseInstrument { this.initDuration = 3000; this.needValidationAfterInit = true; + + this.controlPadHandler = new ControlpadInputController(this.bus, ControlpadTargetInstrument.MFD); } /** @@ -293,6 +302,9 @@ class WTG1000_MFD extends BaseInstrument { super.connectedCallback(); this.airframeOptions.parseConfig(); + // The NXi does not support FMS-managed speed. + SimVar.SetSimVarValue('L:XMLVAR_SpeedIsManuallySet', SimVarValueType.Bool, 1); + this.verticalPathCalculator = new SmoothingPathCalculator(this.bus, this.planner, 0, { defaultFpa: 3, maxFpa: 6, @@ -302,12 +314,28 @@ class WTG1000_MFD extends BaseInstrument { invalidateDescentConstraint: GarminVNavUtils.invalidateDescentConstraint }); - const apConfig = new GarminAPConfig(this.bus, this.planner, this.verticalPathCalculator); + const apConfig = new GarminAPConfig(this.bus, this.planner, this.verticalPathCalculator, { + rollMinBankAngle: this.airframeOptions.autopilotConfig.rollOptions.minBankAngle, + rollMaxBankAngle: this.airframeOptions.autopilotConfig.rollOptions.maxBankAngle, + hdgMaxBankAngle: this.airframeOptions.autopilotConfig.hdgOptions.maxBankAngle, + vorMaxBankAngle: this.airframeOptions.autopilotConfig.vorOptions.maxBankAngle, + locMaxBankAngle: this.airframeOptions.autopilotConfig.locOptions.maxBankAngle, + lnavMaxBankAngle: this.airframeOptions.autopilotConfig.lnavOptions.maxBankAngle, + lowBankAngle: this.airframeOptions.autopilotConfig.lowBankOptions.maxBankAngle, + }); this.autopilot = new G1000Autopilot( this.bus, this.planner, apConfig, new GarminAPStateManager(this.bus, apConfig), - PFDUserSettings.getManager(this.bus) + { + metricAltSettingsManager: PFDUserSettings.getManager(this.bus), + altSelectOptions: { + accelInputCountThreshold: 11, + accelResetOnDirectionChange: true, + accelFilter: AltitudeSelectManagerAccelFilter.ZeroIncDec, + transformSetToIncDec: this.airframeOptions.autopilotConfig.supportAltSelCompatibility + } + } ); Wait.awaitSubscribable(GameStateProvider.get(), state => state === GameState.briefing || state === GameState.ingame).then(() => { @@ -317,6 +345,7 @@ class WTG1000_MFD extends BaseInstrument { }); this.fms = new Fms(true, this.bus, this.planner, this.verticalPathCalculator); + this.goAroundManager = new GarminGoAroundManager(this.bus, this.fms); const softKeyMenuSystem = new SoftKeyMenuSystem(this.bus, 'AS1000_MFD_SOFTKEYS_'); const rotaryMenuSystem = new PageSelectMenuSystem(); @@ -327,7 +356,10 @@ class WTG1000_MFD extends BaseInstrument { this.pluginSystem.addScripts(this.xmlConfig, this.templateID, (target) => { return target === this.templateID; }).then(() => { - this.pluginSystem.startSystem({ bus: this.bus, viewService: this.viewService, menuSystem: softKeyMenuSystem, pageSelectMenuSystem: rotaryMenuSystem }).then(() => { + this.pluginSystem.startSystem({ + bus: this.bus, backplane: this.backplane, fms: this.fms, viewService: this.viewService, + menuSystem: softKeyMenuSystem, pageSelectMenuSystem: rotaryMenuSystem + }).then(() => { softKeyMenuSystem.addMenu('empty', new SoftKeyMenu(softKeyMenuSystem)); softKeyMenuSystem.addMenu('navmap-root', new MFDNavMapRootMenu(softKeyMenuSystem)); @@ -353,9 +385,14 @@ class WTG1000_MFD extends BaseInstrument { softKeyMenuSystem.pushMenu('navmap-root'); - this.pluginSystem.callPlugins(p => p.onMenuSystemInitialized()); + this.pluginSystem.callPlugins(p => p.onMenuSystemInitialized?.()); - FSComponent.render(, document.getElementsByClassName('eis')[0] as HTMLDivElement); + let eisNode: VNode | null = null; + this.pluginSystem.callPlugins(p => { + eisNode ??= p.renderEIS?.() || null; + }); + + FSComponent.render(eisNode ?? , document.getElementsByClassName('eis')[0] as HTMLDivElement); FSComponent.render( , document.getElementById('NavComBox') ); - FSComponent.render(, document.querySelector('#NavComBox #Left')); - FSComponent.render(, document.querySelector('#NavComBox #Right')); + FSComponent.render(, document.querySelector('#NavComBox #Left')); + FSComponent.render(, document.querySelector('#NavComBox #Right')); FSComponent.render(, document.getElementById('Electricity')); FSComponent.render( this.initAcknowledged = true} />, this); @@ -413,9 +450,11 @@ class WTG1000_MFD extends BaseInstrument { // this.viewService.registerView('Turb', () => ); + this.controlPadHandler.setFrequencyElementRefs(this.comRadio.instance, this.navRadio.instance); + this.viewService.open('NavMapPage'); - this.pluginSystem.callPlugins(p => p.onViewServiceInitialized()); + this.pluginSystem.callPlugins(p => p.onViewServiceInitialized?.()); this.controlPublisher.publishEvent('init_cdi', true); }); @@ -460,6 +499,21 @@ class WTG1000_MFD extends BaseInstrument { } }); + //Bridge G1000 CDI to newer nav source events for GoAoundManager + this.bus.getSubscriber().on('cdi_select').handle(v => { + const publisher = this.bus.getPublisher(); + + if (v.type === NavSourceType.Gps) { + publisher.pub('active_nav_source_1', ActiveNavSource.Gps1, false, true); + } else if (v.type === NavSourceType.Nav) { + publisher.pub('active_nav_source_1', v.index === 0 ? ActiveNavSource.Nav1 : ActiveNavSource.Nav2); + } else { + publisher.pub('active_nav_source_1', ActiveNavSource.Nav1); + } + + }); + this.goAroundManager?.init(); + this.doDelayedInit(); } @@ -583,8 +637,14 @@ class WTG1000_MFD extends BaseInstrument { * @param args The H event and associated arguments, if any. */ public onInteractionEvent(args: string[]): void { - this.hEventPublisher.dispatchHEvent(args[0]); + const isHandled = this.controlPadHandler.handleControlPadEventInput(args[0]); + if (isHandled === false) { + // If the controlpad handler did not handle the event, continue and publish: + this.hEventPublisher.dispatchHEvent(args[0]); + } } + + } -registerInstrument('wtg1000-mfd', WTG1000_MFD); \ No newline at end of file +registerInstrument('wtg1000-mfd', WTG1000_MFD); diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator.css deleted file mode 100644 index 87419816d..000000000 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator.css +++ /dev/null @@ -1,260 +0,0 @@ -.airspeed { - transform: rotateX(0); - position: absolute; - top: 111px; - height: 363px; - left: 152px; - width: 90px; - background: linear-gradient(to bottom, rgba(0, 0, 0, .5), rgba(0, 0, 0, 0), rgba(0, 0, 0, .5)); - border-top-left-radius: 10px; - border-bottom-left-radius: 10px; -} - -.airspeed-top-border { - position: absolute; - top: -1px; - left: -1px; - height: 15px; - width: 91px; - border: 1px solid rgb(100, 100, 100); - border-top-left-radius: 10px; - border-bottom: none; -} - -.airspeed-middle-border { - position: absolute; - top: 14px; - left: -1px; - height: 320px; - width: 91px; - border-top: none; - border-image: linear-gradient(to bottom, rgba(100, 100, 100, 1), rgba(0, 0, 0, 1)) 1 100%; - border-width: 1px; - border-bottom-width: 0; - overflow: hidden; -} - -.speed-tape { - position: relative; - left: 90%; - bottom: 3.3%; - width: 8px; - height: 45.9%; -} - -#barberpole { - position: absolute; - right: 0; - width: 9px; - background: repeating-linear-gradient(315deg, white, white 10px, red 0px, red 19px); -} - -.tick-marks, -.airspeed-trend-vector { - position: absolute; - left: 18px; - top: 0; - width: 70px; - height: 92%; - overflow: hidden; -} - -.airspeed-trend-vector { - left: unset; - right: -6px; -} - -.flc-box { - position: absolute; - width: 89px; - height: 8%; - top: 0; - font-size: 20px; - border-top-left-radius: 10px; - background-color: black; - border-bottom: none; -} - -.flc-value { - position: absolute; - top: 2px; - left: 30px; - text-align: right; - width: 50px; -} - -.ias-box { - position: absolute; - top: 132px; - width: 100%; - height: 70px; - border: none; - fill: none; - --speedNumberColor: white; -} - -.scroller-background { - position: absolute; - top: 17px; - width: 17px; - border: none; - text-align: center; - font-size: 32px; - background: linear-gradient(to bottom, rgba(0, 0, 0, 1.0), rgb(30, 30, 30), rgba(0, 0, 0, 1.0)); - overflow: hidden; -} - -.scroller-background svg { - height: 500px -} - -.overspeed.ias-box path { - fill: red; - stroke: none; -} - -.overspeed .scroller-background, -.overspeed .ones-scroller-mask { - background: none; -} - -.hundreds-scroller { - position: absolute; - left: 14px; - height: 35px; -} - -.tens-scroller { - position: absolute; - left: 34px; - height: 35px; - padding-bottom: 5px; -} - -.ones-scroller { - position: absolute; - z-index: 10; - top: 6px; - left: 54px; - height: 57px; -} - -.ones-scroller-mask { - position: absolute; - top: -4px; - width: 17px; - height: 65px; - overflow: hidden; - padding-top: 4px; -} - -.ones-scroller-overlay { - position: absolute; - top: 0; - width: 17px; - height: 65px; - background: linear-gradient(to bottom, rgba(0, 0, 0, 1) 5%, rgba(0, 0, 0, 0) 20%, rgba(0, 0, 0, 0) 80%, rgba(0, 0, 0, 1) 95%); -} - -.flc-bug { - position: absolute; - right: 0px; - top: 157px; -} - -.airspeed-bug { - position: absolute; - font-size: 16px; - color: cyan; -} - -.bug-values { - position: absolute; - font-size: 20px; - right: 8px; -} - -.vspeed-bug-container { - position: absolute; - height: 335px; - width: 30px; - left: 92px; - top: 0px; - overflow: hidden; -} - -.vspeed-values-background { - position: absolute; - width: 89px; - height: 169px; - top: 151px; - text-align: right; - background: rgba(16, 15, 14, .7); -} - -.glide, -.vr, -.vx, -.vy { - top: 157px; -} - -.value1 { - position: absolute; - bottom: 4px; -} - -.value2 { - position: absolute; - bottom: 26px; -} - -.value3 { - position: absolute; - bottom: 48px; -} - -.value4 { - position: absolute; - bottom: 70px; -} - -.tas-box { - position: absolute; - top: 92%; - width: 91px; - height: 8%; - left: -1px; - padding: 10% 5%; - font-size: 12px; - border-bottom-left-radius: 10px; - background-color: black; - overflow: hidden; - transform: rotateX(0); -} - -.tas-value { - position: absolute; - text-align: right; - top: 10%; - right: 5%; - font-size: 20px; -} - -/** Fail States **/ - -.failed-instr .ias-box { - display: none; -} - -.failed-instr .speed-tape-numers { - display: none; -} - -.failed-instr .tas-box { - display: none; -} - -.failed-instr .vspeed-values-container { - display: none; -} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator.tsx deleted file mode 100644 index c5fc55b09..000000000 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator.tsx +++ /dev/null @@ -1,883 +0,0 @@ -import { AdcEvents, APEvents, APLockType, ComputedSubject, DisplayComponent, EventBus, FSComponent, MathUtils, NodeReference, Subject, VNode } from '@microsoft/msfs-sdk'; - -import { G1000ControlEvents } from '../../../Shared/G1000Events'; -import { ADCSystemEvents } from '../../../Shared/Systems/ADCAvionicsSystem'; -import { AvionicsSystemState, AvionicsSystemStateEvent } from '../../../Shared/Systems/G1000AvionicsSystem'; - -import './AirspeedIndicator.css'; - -/** - * A Vspeed type. - */ -export enum VSpeedType { - Vx, - Vy, - Vglide, - Vapp, - Vr -} - -enum SpeedWarning { - None, - Caution, - Overspeed -} - -/** - * A Vspeed Object. - */ -export class VSpeed { - type: VSpeedType; - value: number; - modified = Subject.create(false); - display: boolean | undefined; - - /** - * Creates an instance of a VSpeed. - * @param type is the type of vspeed - * @param value is the value of a vspeed in knots - * @param modified is whether the pilot has modified this vspeed - * @param display is whether this vspeed should be displayed - */ - constructor(type: VSpeedType, value = 0, modified = false, display: boolean | undefined = undefined) { - this.type = type; - this.value = value; - this.modified.set(modified); - this.display = display; - } -} - -/** - * The properties on the airspeed component. - */ -interface AirspeedIndicatorProps { - - /** An instance of the event bus. */ - bus: EventBus; -} - -/** - * The PFD airspeed indicator with speed tape. - */ -export class AirspeedIndicator extends DisplayComponent { - - private containerRef = FSComponent.createRef(); - private airspeedHundredsDataElement = FSComponent.createRef(); - private airspeedTensDataElement = FSComponent.createRef(); - private airspeedOnesDataElement = FSComponent.createRef(); - private airspeedTapeTickElement = FSComponent.createRef(); - private airspeedBoxElement = FSComponent.createRef(); - private airspeedTrendVector = FSComponent.createRef(); - private currentDrawnIas = 0; - private ias = 0; - private speedTrend = Subject.create(0); - private iasScrollerValues: NodeReference[] = []; - private tasElement = FSComponent.createRef(); - - private speedRangeValues = { - barber: FSComponent.createRef(), - yellow: FSComponent.createRef(), - green: FSComponent.createRef(), - greenWhite: FSComponent.createRef(), - white: FSComponent.createRef(), - red: FSComponent.createRef() - }; - private readonly vxSpeedRef = FSComponent.createRef(); - private readonly vySpeedRef = FSComponent.createRef(); - private readonly vgSpeedRef = FSComponent.createRef(); - private readonly vrSpeedRef = FSComponent.createRef(); - private readonly vappSpeedRef = FSComponent.createRef(); - private readonly vSpeedBackgroundRef = FSComponent.createRef(); - private readonly vSpeedContainerRef = FSComponent.createRef(); - private readonly flcBugRef = FSComponent.createRef(); - private readonly flcBoxRef = FSComponent.createRef(); - - private readonly hundredsSvg = FSComponent.createRef(); - private readonly tensSvg = FSComponent.createRef(); - private readonly onesSvg = FSComponent.createRef(); - - private flcSubject = Subject.create('- - - '); - private stallDirty = Simplane.getDesignSpeeds().VS0; - private stallClean = Simplane.getDesignSpeeds().VS1; - private flapsExtend = Simplane.getDesignSpeeds().VFe; - private yellowSpeed = Simplane.getDesignSpeeds().VNo; - private barberSpeed = Simplane.getDesignSpeeds().VNe; - - private flcSpeed = 0; - private vSpeeds: VSpeed[] = [ - { type: VSpeedType.Vx, value: Simplane.getDesignSpeeds().Vx, modified: Subject.create(false), display: true }, - { type: VSpeedType.Vy, value: Simplane.getDesignSpeeds().Vy, modified: Subject.create(false), display: true }, - { type: VSpeedType.Vr, value: Simplane.getDesignSpeeds().Vr, modified: Subject.create(false), display: true }, - { type: VSpeedType.Vglide, value: Simplane.getDesignSpeeds().BestGlide, modified: Subject.create(false), display: true }, - { type: VSpeedType.Vapp, value: Simplane.getDesignSpeeds().Vapp, modified: Subject.create(false), display: false } - ]; - - private vSpeedSubjects: Subject[] = [ - Subject.create('0'), - Subject.create('0'), - Subject.create('0'), - Subject.create('0'), - Subject.create('0') - ]; - - private speedWarningSubject = Subject.create(SpeedWarning.None); - private isFailed = false; - - private tasSubject = Subject.create(0); - - /** - * Builds a numerical scroller for the airspeed window. - * @param startYValue The starting Y value in the svg to start number at. - * @param blankZeroValue Whether or not the 0 digit should be replaced by an empty space. - * @returns A collection of text elements for the numerical scroller. - */ - private buildScroller(startYValue = -2, blankZeroValue = false): SVGTextElement[] { - const scroller: SVGTextElement[] = []; - let yValue = startYValue; - - for (let i = 11; i > -3; i--) { - const number = i > 9 ? i - 10 : i < 0 ? i + 10 : i; - let numberText = number.toString(); - - if (blankZeroValue && number === 0) { - numberText = ' '; - } - - scroller.push({numberText}); - yValue += 30; - } - - scroller.push(); - return scroller; - } - - /** - * Builds the tick marks on the airspeed tape. - * @returns A collection of tick mark line elements. - */ - private buildSpeedTapeTicks(): SVGLineElement[] { - const ticks: SVGLineElement[] = []; - - for (let i = 1; i < 18; i++) { - const length = i % 2 == 0 ? 15 : 30; - - const startX = 94 + (length == 30 ? 0 : 15); - const startY = 450 - (i * 50); - - const endX = startX + length; - const endY = startY; - - ticks.push(); - } - - return ticks; - } - - /** - * Builds the airspeed numbers for the airspeed tape. - * @returns A collection of airspeed number text elements. - */ - private buildSpeedTapeNumbers(): SVGTextElement[] { - const text: SVGTextElement[] = []; - - for (let i = 1; i < 10; i++) { - const startX = 75; - const startY = 513 - (i * 100); - - const numberText = (10 + (i * 10)).toString(); - const textElement = FSComponent.createRef(); - - text.push({numberText}); - this.iasScrollerValues.push(textElement); - } - - return text; - } - - /** - * Builds the airspeed tape color ranges. - * @returns A collection of color range rect elements. - */ - private buildSVGSpeedTapeRanges(): SVGRectElement[] { - const rectangle: SVGRectElement[] = []; - const zeroYValue = 0; - - //red band (full width 8px top left) - const redMin = zeroYValue; - const redMax = 400 - redMin - (10 * (this.stallDirty - 20)); - rectangle.push(); - - const whiteMin = redMax; - const whiteMax = Math.max(-400, whiteMin - (10 * (this.flapsExtend - this.stallDirty))); - rectangle.push(); - - const halfGreenMin = whiteMin - (10 * (this.stallClean - this.stallDirty)); - const halfGreenMax = whiteMax; - rectangle.push(); - - const greenMin = whiteMax; - const greenMax = Math.max(-400, greenMin - (10 * (this.yellowSpeed - this.flapsExtend))); - rectangle.push(); - - const yellowMin = greenMax; - const yellowMax = Math.max(-400, yellowMin - (10 * (this.barberSpeed - this.yellowSpeed))); - rectangle.push(); - - return rectangle; - } - /** - * Builds the airspeed tape color ranges. - * @returns A collection of color range rect elements. - */ - private buildHTMLSpeedTapeRanges(): HTMLDivElement[] { - const divs: HTMLDivElement[] = []; - - divs.push(
); - - return divs; - } - - /** - * A callback called after the component renders. - */ - public onAfterRender(): void { - this.updateSpeedBugs(this.ias); - const adc = this.props.bus.getSubscriber(); - const ap = this.props.bus.getSubscriber(); - - this.speedWarningSubject.sub(this.speedWarningChanged.bind(this)); - - const g1000Events = this.props.bus.getSubscriber(); - g1000Events.on('vspeed_set').handle(this.setVSpeed); - g1000Events.on('vspeed_display').handle(this.setVSpeedVisibility); - - //init the FLC elements to off - this.flcBugRef.instance.style.display = 'none'; - this.flcBoxRef.instance.style.display = 'none'; - - this.props.bus.getSubscriber() - .on('adc_state') - .handle(this.onAdcStateChanged.bind(this)); - - this.speedTrend.sub(this.onSpeedTrendChanged.bind(this)); - - adc.on('ias') - .withPrecision(2) - .handle(this.onUpdateIAS); - adc.on('tas') - .withPrecision(0) - .handle(this.onUpdateTAS); - ap.on('ap_ias_selected') - .whenChangedBy(1) - .handle(this.setFlcBug); - ap.on('ap_lock_set') - .handle(this.toggleFlcElements); - } - - /** - * A callaback called when the system screen state changes. - * @param state The state change event to handle. - */ - private onAdcStateChanged(state: AvionicsSystemStateEvent): void { - if (state.previous === undefined && state.current !== AvionicsSystemState.Off) { - this.setFailed(false); - } else { - if (state.current === AvionicsSystemState.On) { - this.setFailed(false); - } else { - this.setFailed(true); - } - } - } - - /** - * Sets if the display should be failed or not. - * @param isFailed True if failed, false otherwise. - */ - private setFailed(isFailed: boolean): void { - if (isFailed) { - this.isFailed = true; - this.containerRef.instance.classList.add('failed-instr'); - } else { - this.isFailed = false; - this.containerRef.instance.classList.remove('failed-instr'); - } - } - - /** - * A callback called when the speedwarning state changes. - * @param state the speed warning state - */ - private speedWarningChanged(state: SpeedWarning): void { - switch (state) { - case SpeedWarning.None: - this.airspeedBoxElement.instance.classList.remove('overspeed'); - this.airspeedBoxElement.instance.style.setProperty('--speedNumberColor', 'white'); - break; - case SpeedWarning.Caution: - this.airspeedBoxElement.instance.style.setProperty('--speedNumberColor', 'yellow'); - break; - case SpeedWarning.Overspeed: - this.airspeedBoxElement.instance.style.setProperty('--speedNumberColor', 'white'); - this.airspeedBoxElement.instance.classList.add('overspeed'); - break; - } - } - - /** - * A method called when a flc speed value changes. - * @param speed The flc ias speed value. - */ - private setFlcBug = (speed: number): void => { - this.flcSpeed = Math.round(speed); - this.flcSubject.set(`${this.flcSpeed}`); - this.updateFlcBug(); - }; - - /** - * A method called to update the location of the FLC Bug on the tape. - */ - private updateFlcBug(): void { - let deltaBug = this.flcSpeed - this.ias; - if (this.ias < 20) { - deltaBug = this.flcSpeed < 20 ? 0 : this.flcSpeed - 20; - } else if (this.ias < 50) { - deltaBug = Math.max(this.flcSpeed - (this.ias - 20), -30); - } - this.flcBugRef.instance.style.transform = `translate3d(0,${-5.583 * Utils.Clamp(deltaBug, -30, 25)}px,0)`; - } - - /** - * A method called to update the location of the FLC Bug on the tape. - * @param mode The enum of the ap mode - */ - private toggleFlcElements = (mode: APLockType): void => { - switch (mode) { - case APLockType.Flc: - this.flcBugRef.instance.style.display = ''; - this.flcBoxRef.instance.style.display = ''; - break; - case APLockType.Pitch: - case APLockType.Vs: - case APLockType.Alt: - case APLockType.Glideslope: - this.flcBugRef.instance.style.display = 'none'; - this.flcBoxRef.instance.style.display = 'none'; - break; - } - }; - - /** - * A method called when a vspeed update event from the event bus is received. - * @param vSpeed The vSpeed object to change. - */ - private setVSpeed = (vSpeed: VSpeed): void => { - const index = this.vSpeeds.findIndex((type) => { - if (type.type === vSpeed.type) { return true; } - }); - this.vSpeeds[index].value = vSpeed.value; - this.vSpeedSubjects[index].set(`${vSpeed.value}`); - this.updateSpeedBugs(this.ias); - }; - - /** - * A method called when a vspeed display event from the event bus is received. - * @param vSpeed The vSpeed object to change. - */ - private setVSpeedVisibility = (vSpeed: VSpeed): void => { - const index = this.vSpeeds.findIndex((speed) => { - if (speed.type === vSpeed.type) { return true; } - }); - this.vSpeeds[index].display = vSpeed.display === undefined ? false : vSpeed.display; - - - /** - * Handle updating visibility for each element. - * @param speed instance to update - */ - const setVisibility = (speed: VSpeed): void => { - const instance = speed.type === VSpeedType.Vx ? this.vxSpeedRef.instance : - speed.type === VSpeedType.Vy ? this.vySpeedRef.instance : - speed.type === VSpeedType.Vr ? this.vrSpeedRef.instance : - speed.type === VSpeedType.Vglide ? this.vgSpeedRef.instance : undefined; - // speed.type === VSpeedType.Vapp ? this.vappSpeedRef.instance : undefined; - - if (instance !== null && instance !== undefined) { - instance.style.display = speed.display ? '' : 'none'; - } - }; - this.vSpeeds.forEach(setVisibility); - this.updateSpeedBugs(this.ias); - }; - - /** - * Determines whether speed trend is in the overspeed range. - * @param ias the current ias - * @returns true if speed trend in overspeed - */ - private isSpeedTrendInOverspeed(ias: number): boolean { - return ias + (this.speedTrend.get() / 10) >= this.barberSpeed; - } - - private readonly iasHundredsTranslate = ComputedSubject.create(0, (trans: number) => { - return `translate(0,${trans})`; - }); - private readonly iasTensTranslate = ComputedSubject.create(0, (trans: number) => { - return `translate(0,${trans})`; - }); - private readonly iasOnesTranslate = ComputedSubject.create(0, (trans: number) => { - return `translate(0,${trans})`; - }); - - /** - * A callback called when the IAS updates from the event bus. - * @param ias The current IAS value. - */ - private onUpdateIAS = (ias: number): void => { - if (this.isFailed) { - ias = 0; - } - - this.updateTrendVector(ias); - - if (ias <= this.barberSpeed) { - this.speedWarningSubject.set(this.isSpeedTrendInOverspeed(ias) ? SpeedWarning.Caution : SpeedWarning.None); - } else { - this.speedWarningSubject.set(SpeedWarning.Overspeed); - } - - this.ias = ias; - const ones = ias % 10; - const tens = (ias % 100 - ones) / 10; - const hundreds = (ias - tens * 10 - ones) / 100; - - if (this.airspeedHundredsDataElement.instance !== null) { - let newTranslation = -300 + (hundreds * 30); - if (tens === 9 && ones > 9) { - newTranslation -= ((10 - ones) * 30) - 30; - } - if (ias < 20) { - newTranslation = -420; - } - this.hundredsSvg.instance.style.transform = `translate3d(0px, ${newTranslation}px, 0px)`; - } - - if (this.airspeedTensDataElement.instance !== null) { - let newTranslation = -300 + (tens * 30); - if (ones > 9) { - newTranslation -= ((10 - ones) * 30) - 30; - } - if (ias < 20) { - newTranslation = -420; - } - this.tensSvg.instance.style.transform = `translate3d(0px, ${newTranslation}px, 0px)`; - } - - if (this.airspeedOnesDataElement.instance !== null) { - let newTranslation = -300 + (ones * 30); - if (ias < 20) { - newTranslation = -420; - } - this.onesSvg.instance.style.transform = `translate3d(0px, ${newTranslation}px, 0px)`; - } - - if (this.airspeedTapeTickElement.instance !== null) { - let newTranslation = 10 * ((ias % 10)); - if (ias < 20) { - newTranslation = -400; - } else if (ias < 50) { - newTranslation = -400 + (10 * (ias - 20)); - } - this.airspeedTapeTickElement.instance.style.transform = `translate3d(0px, ${(newTranslation - 120.6) * (446 / 800)}px, 0px)`; - } - - if (ias >= 50 && (ias / 10 >= this.currentDrawnIas + 1 || ias / 10 < this.currentDrawnIas)) { - this.currentDrawnIas = Math.floor(ias / 10); - for (let i = 0; i < this.iasScrollerValues.length; i++) { - const scrollerValue = this.iasScrollerValues[i].instance; - if (scrollerValue !== null) { - scrollerValue.textContent = (10 * ((i - 4) + this.currentDrawnIas)).toString(); - } - } - this.updateSpeedRanges(ias); - } else if (ias < 50 && (ias / 10 > this.currentDrawnIas + 1 || ias / 10 < this.currentDrawnIas)) { - this.currentDrawnIas = Math.floor(ias / 10); - for (let i = 0; i < this.iasScrollerValues.length; i++) { - const scrollerValue = this.iasScrollerValues[i].instance; - if (scrollerValue !== null) { - scrollerValue.textContent = ((i + 2) * 10).toString(); - } - } - this.updateSpeedRanges(ias, true); - } - this.updateSpeedBugs(ias); - this.updateFlcBug(); - }; - - private _lastIAS = 0; - private _lastTime = 0; - private _computedIASAcceleration = 0; - /** - * A computation of the current IAS Acceleration used for the Airspeed Trend Vector. - * @param ias The current IAS value. - * @returns The current IAS Acceleration. - */ - private computeIASAcceleration = (ias: number): number => { - const newIASTime = { - ias: ias, - t: performance.now() / 1000 - }; - if (this._lastTime == 0) { - this._lastIAS = ias; - this._lastTime = performance.now() / 1000; - return 0; - } - let frameIASAcceleration = (newIASTime.ias - this._lastIAS) / (newIASTime.t - this._lastTime); - frameIASAcceleration = MathUtils.clamp(frameIASAcceleration, -10, 10); - if (isFinite(frameIASAcceleration)) { - this._computedIASAcceleration += (frameIASAcceleration - this._computedIASAcceleration) / (50 / ((newIASTime.t - this._lastTime) / .016)); - } - this._lastIAS = ias; - this._lastTime = performance.now() / 1000; - const accel = this._computedIASAcceleration * 6; - return accel; - }; - - /** - * Updates the Airspeed Trend Vector. - * @param ias The current IAS value. - */ - private updateTrendVector(ias: number): void { - this.speedTrend.set((ias >= 20) ? this.computeIASAcceleration(ias) * 5 : 0); - } - - /** - * Callback for when the speed trend changes. - * @param speedTrend The current speed trend. - */ - private onSpeedTrendChanged(speedTrend: number): void { - const verticalOffset = -117 - Math.max(0, speedTrend); - - this.airspeedTrendVector.instance.setAttribute('y', verticalOffset.toString()); - this.airspeedTrendVector.instance.setAttribute('height', Math.abs(speedTrend).toString()); - } - - /** - * Updates the speed bugs on the speed tape. (displayed if within 30 kts of ias) - * @param ias The current IAS value. - */ - private updateSpeedBugs(ias: number): void { - if (this.ias < 20) { - /** - * Sort function for the v speeds - * @param a the first value to compare - * @param b the second value to compare - * @returns the sorted array - */ - const sortSpeeds = (a: VSpeed, b: VSpeed): number => { - const speedA = a.value; - const speedB = b.value; - let comparison = 0; - if (speedA > speedB) { - comparison = 1; - } else if (speedA < speedB) { - comparison = -1; - } - return comparison; - }; - - const sortedSpeeds = this.vSpeeds.sort(sortSpeeds); - - let offset = 153; - let value = 0; - for (let i = 0; i < 5; i++) { - if (sortedSpeeds[i] && sortedSpeeds[i].display) { - const instance = sortedSpeeds[i].type === VSpeedType.Vx ? this.vxSpeedRef.instance : - sortedSpeeds[i].type === VSpeedType.Vy ? this.vySpeedRef.instance : - sortedSpeeds[i].type === VSpeedType.Vr ? this.vrSpeedRef.instance : - sortedSpeeds[i].type === VSpeedType.Vglide ? this.vgSpeedRef.instance : undefined; - // this.vSpeeds[i].type === VSpeedType.Vapp ? this.vappSpeedRef.instance : undefined; - - if (instance !== null && instance !== undefined) { - instance.style.transform = `translate3d(0,${offset}px,0)`; - } - this.vSpeedSubjects[value].set('' + Math.round(sortedSpeeds[i].value)); - - offset -= 22; - value++; - } - } - if (value < 4) { - for (let j = value; j < 4; j++) { - this.vSpeedSubjects[j].set(''); - } - } - this.vSpeedContainerRef.instance.style.display = ''; - } else { - this.vSpeedContainerRef.instance.style.display = 'none'; - const vxIndex = this.vSpeeds.findIndex((type) => { - if (type.type === VSpeedType.Vx) { return true; } - }); - const vyIndex = this.vSpeeds.findIndex((type) => { - if (type.type === VSpeedType.Vy) { return true; } - }); - const vrIndex = this.vSpeeds.findIndex((type) => { - if (type.type === VSpeedType.Vr) { return true; } - }); - const vgIndex = this.vSpeeds.findIndex((type) => { - if (type.type === VSpeedType.Vglide) { return true; } - }); - // const vappIndex = this.vSpeeds.findIndex((type) => { - // if (type.type === VSpeedType.Vapp) { return true; } - // }); - const deltaVx = this.vSpeeds[vxIndex].value - ias; - const deltaVy = this.vSpeeds[vyIndex].value - ias; - const deltaVr = this.vSpeeds[vrIndex].value - ias; - const deltaVg = this.vSpeeds[vgIndex].value - ias; - // const deltaVapp = this.vSpeeds[vappIndex].value - ias; - - if (Math.abs(deltaVx) < 35) { - this.vxSpeedRef.instance.style.transform = `translate3d(0,${-5.583 * deltaVx}px,0)`; - } else { - this.vxSpeedRef.instance.style.transform = `translate3d(0,${-5.583 * 35}px,0)`; - } - if (Math.abs(deltaVy) < 35) { - this.vySpeedRef.instance.style.transform = `translate3d(0,${-5.583 * deltaVy}px,0)`; - } else { - this.vySpeedRef.instance.style.transform = `translate3d(0,${-5.583 * 35}px,0)`; - } - if (Math.abs(deltaVr) < 35) { - this.vrSpeedRef.instance.style.transform = `translate3d(0,${-5.583 * deltaVr}px,0)`; - } else { - this.vrSpeedRef.instance.style.transform = `translate3d(0,${-5.583 * 35}px,0)`; - } - if (Math.abs(deltaVg) < 35) { - this.vgSpeedRef.instance.style.transform = `translate3d(0,${-5.583 * deltaVg}px,0)`; - } else { - this.vgSpeedRef.instance.style.transform = `translate3d(0,${-5.583 * 35}px,0)`; - } - // if (Math.abs(deltaVapp) < 30) { - // this.vappSpeedRef.instance.style.transform = `translate3d(0,${10 * deltaVapp}px,0)`; - // } - } - - if (this.ias >= 0 && this.ias < 50) { - this.vSpeedBackgroundRef.instance.style.display = 'block'; - if (this.ias >= 20) { - this.vSpeedBackgroundRef.instance.style.transform = `translate3d(0,${5.583 * (ias - 20)}px,0)`; - } - } else { - this.vSpeedBackgroundRef.instance.style.display = 'none'; - } - } - - /** - * A callback called when the TAS updates from the event bus. - * @param tas The current TAS value. - */ - private onUpdateTAS = (tas: number): void => { - this.tasSubject.set(tas); - }; - - /** - * Updates the speed range color bars and the IAS box style. - * @param ias The current IAS value. - * @param slowSpeed Whether or not the IAS is in the slow speed range. - */ - private updateSpeedRanges(ias: number, slowSpeed = false): void { - for (let i = 1; i < 7; i++) { - let instance: SVGRectElement | HTMLDivElement | null; - let rangeBottom: number; - let rangeTop: number; - let isSVG = true; - - switch (i) { - case 1: - instance = this.speedRangeValues.red.instance; - rangeBottom = 20; - rangeTop = this.stallDirty; - break; - case 2: - instance = this.speedRangeValues.white.instance; - rangeBottom = this.stallDirty; - rangeTop = this.flapsExtend; - break; - case 3: - instance = this.speedRangeValues.greenWhite.instance; - rangeBottom = this.stallClean; - rangeTop = this.flapsExtend; - break; - case 4: - instance = this.speedRangeValues.green.instance; - rangeBottom = this.flapsExtend; - rangeTop = this.yellowSpeed; - break; - case 5: - instance = this.speedRangeValues.yellow.instance; - rangeBottom = this.yellowSpeed; - rangeTop = this.barberSpeed; - break; - case 6: - instance = this.speedRangeValues.barber.instance; - rangeBottom = this.barberSpeed; - rangeTop = Infinity; - isSVG = false; - break; - default: - return; - } - - if (ias < 40 + rangeTop && ias > rangeBottom - 41 && instance !== null) { - let top: number; - let bottom: number; - if (slowSpeed) { - top = Math.max(-400, 400 - (10 * (rangeTop - 20))); - bottom = Math.min(400, 400 - (10 * (rangeBottom - 20))); - } else { - top = Math.max(-400, 10 * ((10 * this.currentDrawnIas) - rangeTop)); - bottom = Math.min(400, 10 * ((10 * this.currentDrawnIas) - rangeBottom)); - } - if (isSVG) { - instance.setAttribute('y', `${top}`); - instance.setAttribute('height', `${bottom - top}`); - } else { - instance.style.top = (233 + top * 0.5575).toString() + 'px'; - instance.style.height = Math.max(0, ((bottom - top) * 0.5575)).toString() + 'px'; - } - instance.style.display = ''; - } else if (instance !== null) { - instance.style.display = 'none'; - } - } - } - // private readonly vxSpeedRef = FSComponent.createRef(); - // private readonly vySpeedRef = FSComponent.createRef(); - // private readonly vgSpeedRef = FSComponent.createRef(); - // private readonly vrSpeedRef = FSComponent.createRef(); - // private readonly vappSpeedRef = FSComponent.createRef(); - /** - * Renders the component. - * @returns The component VNode. - */ - public render(): VNode { - return ( -
-
-
-
-
-
{this.vSpeedSubjects[0]}
-
{this.vSpeedSubjects[1]}
-
{this.vSpeedSubjects[2]}
-
{this.vSpeedSubjects[3]}
-
-
-
-
-
-
- - - {this.buildSVGSpeedTapeRanges()} - {this.buildSpeedTapeTicks()} - - {this.buildSpeedTapeNumbers()} - - - - {this.buildHTMLSpeedTapeRanges()} -
-
- -
-
- - - -
-
- -
- - - - -
- - {this.buildScroller(-2, true)} - -
- -
- - {this.buildScroller()} - -
- -
-
- - {this.buildScroller(9)} - -
-
-
-
- -
- - - -
- -
-
- - - G - -
- -
- - - R - -
- -
- - - X - -
- - -
- - - Y - -
-
- -
- - - - -
{this.flcSubject}KT
- -
- -
TAS -
- {this.tasSubject} - KT -
-
- -
- ); - } -} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicator.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicator.css new file mode 100644 index 000000000..7c3b974fa --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicator.css @@ -0,0 +1,679 @@ +.airspeed { + position: absolute; + left: 154px; + top: 82px; + /* width: 90px; */ + /* height: 363px; */ + + width: 87px; + height: 390px; + font-family: "DejaVuSans-SemiBold"; + font-size: 21px; + transform: rotateX(0deg); + + --airspeed-ias-box-height: 70px; + --digit-scroller-line-height: 1.2em; + --digit-scroller-line-offset-y: 2px; + + /* Top/bottom box vars */ + + --airspeed-top-box-height: 31px; + --airspeed-bottom-box-height: 28px; + + --airspeed-refspeed-font-size: 0.8em; + --airspeed-refspeed-bg: rgba(0, 0, 0, 0.83); + --airspeed-refspeed-icon-margin-left: 6px; + --airspeed-refspeed-icon-width: 8px; + --airspeed-refspeed-icon-height: 16px; + + --airspeed-alert-font-size: 0.8em; + --airspeed-alert-border-radius: 3px; + + --airspeed-bottom-speed-display-bg: rgba(0, 0, 0, 0.83); + + --airspeed-tas-display-font-size: 0.8em; + --airspeed-tas-display-title-offset-y: 0.1em; + --airspeed-tas-display-title-font-size: 0.6em; + + --airspeed-mach-display-font-size: 0.8em; + + --airspeed-vspeed-annunciation-font-size: 0.8em; + + /* Tape vars */ + + --airspeed-tape-bg: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.5)); + --airspeed-tape-bg-border-radius: 10px; + --airspeed-tape-top-border-color: #646464; + --airspeed-tape-bottom-border-color: #2c2c2c; + + --airspeed-tick-minor-width: 12%; + --airspeed-tick-minor-stroke-width: 2px; + --airspeed-tick-major-width: 24%; + --airspeed-tick-major-stroke-width: 2px; + + --airspeed-tape-label-margin-right: 0.5em; + --airspeed-tape-label-font-size: 1em; + + --airspeed-tape-overflow-background: rgba(30, 30, 30, 0.5); + + /* IAS display box vars */ + + --airspeed-ias-box-margin-right: 0%; + --airspeed-ias-box-height: 2.4em; + --airspeed-ias-box-font-size: 1.25em; + --airspeed-ias-box-border-width: 1px; + + /* Color range vars */ + + --airspeed-tape-color-range-full-width: 12%; + --airspeed-tape-color-range-half-width: 6%; + + /* Reference speed bug vars */ + + --airspeed-refspeed-bug-manual-right: 0px; + --airspeed-refspeed-bug-manual-height: calc(var(--airspeed-ias-box-font-size) * 0.8); + --airspeed-refspeed-bug-manual-width: calc(var(--airspeed-ias-box-margin-right) + (100% - var(--airspeed-ias-box-margin-right)) * 0.16); + --airspeed-refspeed-bug-manual-fill: cyan; + --airspeed-refspeed-bug-manual-stroke: #505050; + + --airspeed-refspeed-bug-fms-right: calc(var(--airspeed-ias-box-margin-right) + (100% - var(--airspeed-ias-box-margin-right)) * 0.0444); + --airspeed-refspeed-bug-fms-height: calc(var(--airspeed-ias-box-font-size) * 0.55); + --airspeed-refspeed-bug-fms-width: calc((100% - var(--airspeed-ias-box-margin-right)) * 0.15); + --airspeed-refspeed-bug-fms-fill: magenta; + --airspeed-refspeed-bug-fms-stroke: #505050; + + /* Trend vector vars */ + + --airspeed-trend-width: 6px; + + /* Reference V-speed bug vars */ + + --airspeed-vspeed-bug-icon-height: 1.1em; + --airspeed-vspeed-bug-icon-font-size: 0.6em; + --airspeed-vspeed-bug-icon-color: cyan; + --airspeed-vspeed-bug-icon-bg-color: black; + + --airspeed-vspeed-bug-container-width-factor: 0.6; + --airspeed-vspeed-bug-margin-left: 4px; + + --airspeed-vspeed-offscale-container-height: 50%; + --airspeed-vspeed-offscale-label-margin-bottom: 4px; + --airspeed-vspeed-offscale-label-margin-right: 8px; + --airspeed-vspeed-offscale-label-font-size: 0.8em; + + --airspeed-vspeed-legend-container-width-factor: 0.85; + --airspeed-vspeed-legend-container-padding: 3px 1px 3px 2px; + --airspeed-vspeed-legend-margin-bottom: 2px; + --airspeed-vspeed-legend-font-size: 0.7em; + + /* Computed vars */ + + --airspeed-tape-topleft-border-radius: var(--airspeed-tape-bg-border-radius); + --airspeed-tape-bottomleft-border-radius: var(--airspeed-tape-bg-border-radius); + + --airspeed-tape-top-border: 1px solid var(--airspeed-tape-top-border-color); + --airspeed-tape-bottom-border: 1px solid var(--airspeed-tape-bottom-border-color); + + --airspeed-tape-top-border-actual: var(--airspeed-tape-top-border); + --airspeed-tape-bottom-border-actual: var(--airspeed-tape-bottom-border); +} + +.airspeed.airspeed-reference-visible { + --airspeed-tape-topleft-border-radius: 0px; + --airspeed-tape-top-border-actual: none; +} + +.airspeed.airspeed-bottom-display-visible { + --airspeed-tape-bottomleft-border-radius: 0px; + --airspeed-tape-bottom-border-actual: none; +} + +/* ---- Top container displays ---- */ + +.airspeed-top-container { + position: absolute; + top: 0%; + left: 0%; + width: 100%; + height: var(--airspeed-top-box-height); + transform: rotateX(0deg); +} + +.airspeed-refspeed-container { + position: absolute; + left: 0%; + top: 0%; + width: 100%; + height: 100%; + background: var(--airspeed-refspeed-bg); + border-radius: var(--airspeed-tape-bg-border-radius) 0 0 0; + border: var(--airspeed-tape-top-border); + border-bottom: none; +} + +.airspeed-refspeed-container-manual { + --airspeed-refspeed-container-text-color: cyan; +} + +.airspeed-refspeed-container-fms { + --airspeed-refspeed-container-text-color: magenta; +} + +.airspeed-refspeed-icon { + position: absolute; + left: var(--airspeed-refspeed-icon-margin-left); + top: calc(50% - var(--airspeed-refspeed-icon-height) / 2); + width: var(--airspeed-refspeed-icon-width); + height: var(--airspeed-refspeed-icon-height); +} + +.airspeed-refspeed-icon-manual { + fill: var(--airspeed-refspeed-bug-manual-fill); +} + +.airspeed-refspeed-icon-fms { + fill: var(--airspeed-refspeed-bug-fms-fill); +} + +.airspeed-refspeed-text { + position: absolute; + left: 20%; + top: 0%; + width: 80%; + height: 100%; + color: var(--airspeed-refspeed-container-text-color); + font-size: var(--airspeed-refspeed-font-size); + white-space: nowrap; +} + +.airspeed-refspeed-ias, +.airspeed-refspeed-mach { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +@keyframes airspeed-alert-flash { + 0% { + background: yellow; + color: black; + } + + 75% { + background: black; + color: yellow; + } +} + +.airspeed-protection-container { + position: absolute; + top: 0%; + left: 0%; + width: 100%; + height: 100%; + border-radius: var(--airspeed-alert-border-radius); + animation: airspeed-alert-flash 1s infinite step-end; +} + +.airspeed-protection-text { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: var(--airspeed-alert-font-size); +} + +.airspeed-bottom-container { + position: absolute; + left: 0%; + bottom: 0%; + width: 100%; + height: var(--airspeed-bottom-box-height); + transform: rotateX(0deg); + --airspeed-bottom-speed-display-bg-actual: var(--airspeed-bottom-speed-display-bg); + --airspeed-bottom-speed-display-color: white; +} + +.airspeed-alert-overspeed .airspeed-bottom-container, +.airspeed-alert-underspeed .airspeed-bottom-container { + --airspeed-bottom-speed-display-bg-actual: red; +} + +.airspeed-alert-trend-overspeed .airspeed-bottom-container, +.airspeed-alert-trend-underspeed .airspeed-bottom-container { + --airspeed-bottom-speed-display-color: yellow; +} + +/* ---- Bottom container displays ---- */ + +.airspeed-bottom-display { + position: absolute; + left: 0%; + top: 0%; + width: 100%; + height: 100%; + background: var(--airspeed-bottom-speed-display-bg-actual); + border-radius: 0 0 0 var(--airspeed-tape-bg-border-radius); + border: var(--airspeed-tape-bottom-border); + border-top: none; + color: var(--airspeed-bottom-speed-display-color); +} + +.airspeed-tas-display { + font-size: var(--airspeed-tas-display-font-size); + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + align-items: center; +} + +.airspeed-tas-display-title { + margin-top: var(--airspeed-tas-display-title-offset-y); + margin-left: 1px; + font-size: var(--airspeed-tas-display-title-font-size); +} + +.airspeed-tas-display-value { + margin-right: 1px; +} + +.airspeed-mach-display { + font-size: var(--airspeed-mach-display-font-size); +} + +.airspeed-mach-display-value { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + white-space: nowrap; +} + +.airspeed-vspeed-annunciation { + font-size: var(--airspeed-vspeed-annunciation-font-size); +} + +.airspeed-vspeed-annunciation-box { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + padding: 1px 2px; + border-radius: 5px; + color: black; + letter-spacing: -1px; +} + +.airspeed-vspeed-annunciation-takeoff .airspeed-vspeed-annunciation-box { + background: yellow; +} + +.airspeed-vspeed-annunciation-landing .airspeed-vspeed-annunciation-box { + background: white; +} + +/* ---- Airspeed tape ---- */ + +.airspeed-tape-container { + position: absolute; + left: 0%; + top: var(--airspeed-top-box-height); + width: 100%; + height: calc(100% - var(--airspeed-top-box-height) - var(--airspeed-bottom-box-height)); + background: var(--airspeed-tape-bg); + border-radius: var(--airspeed-tape-topleft-border-radius) 0 0 var(--airspeed-tape-bottomleft-border-radius); +} + +.airspeed-tape-border-top { + position: absolute; + left: 0%; + top: 0%; + width: 100%; + height: 50%; + border-radius: var(--airspeed-tape-topleft-border-radius) 0 0 0; + border: var(--airspeed-tape-top-border); + border-top: var(--airspeed-tape-top-border-actual); + border-bottom: none; +} + +.airspeed-tape-border-bottom { + position: absolute; + left: 0%; + bottom: 0%; + width: 100%; + height: 50%; + border-radius: 0 0 0 var(--airspeed-tape-bottomleft-border-radius); + border: var(--airspeed-tape-bottom-border); + border-bottom: var(--airspeed-tape-bottom-border-actual); + border-top: none; +} + +.airspeed-tape-window { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + border-radius: inherit; + border-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) var(--airspeed-tape-bg-border-radius), var(--airspeed-tape-top-border-color) calc(var(--airspeed-tape-bg-border-radius) + 1px), var(--airspeed-tape-bottom-border-color) calc(100% - var(--airspeed-tape-bg-border-radius) - 1px), rgba(0, 0, 0, 0) calc(100% - var(--airspeed-tape-bg-border-radius))) 1 100%; + border-width: 1px; + border-top: 1px none; + border-bottom: 1px none; +} + +.airspeed-tape { + position: absolute; + bottom: 50%; + width: 100%; +} + +.airspeed-tape-tick-minor-container { + position: absolute; + right: 0%; + width: var(--airspeed-tick-minor-width); + height: 100%; + stroke: white; + stroke-width: var(--airspeed-tick-minor-stroke-width); +} + +.airspeed-tape-tick-major-container { + position: absolute; + right: 0%; + width: var(--airspeed-tick-major-width); + height: 100%; + stroke: white; + stroke-width: var(--airspeed-tick-major-stroke-width); +} + +.airspeed-tape-label-container { + position: absolute; + --airspeed-tape-label-container-right: calc(var(--airspeed-tick-major-width) + var(--airspeed-tape-label-margin-right)); + right: var(--airspeed-tape-label-container-right); + width: calc(100% - var(--airspeed-tape-label-container-right)); + height: 100%; + font-size: var(--airspeed-tape-label-font-size); + color: white; + text-align: right; +} + +.airspeed-tape-overflow { + background: var(--airspeed-tape-overflow-background); +} + +.airspeed-ias-box { + position: absolute; + right: var(--airspeed-ias-box-margin-right); + width: calc(100% - var(--airspeed-ias-box-margin-right)); + top: 50%; + height: var(--airspeed-ias-box-height); + transform: translateY(-50%); + font-size: var(--airspeed-ias-box-font-size); + color: white; + --airspeed-ias-box-fill: black; + --airspeed-ias-box-stroke: white; + --airspeed-ias-box-bg: linear-gradient(to bottom, rgba(0, 0, 0, 1.0), rgb(30, 30, 30), rgba(0, 0, 0, 1.0)); + --airspeed-ias-box-scroller-mask-bg: linear-gradient(to bottom, rgba(0, 0, 0, 1) 5%, rgba(0, 0, 0, 0) 20%, rgba(0, 0, 0, 0) 80%, rgba(0, 0, 0, 1) 95%); +} + +.airspeed-alert-overspeed .airspeed-ias-box, +.airspeed-alert-underspeed .airspeed-ias-box { + --airspeed-ias-box-fill: red; + --airspeed-ias-box-stroke: red; + --airspeed-ias-box-bg: red; + --airspeed-ias-box-scroller-mask-bg: transparent; +} + +.airspeed-alert-trend-overspeed .airspeed-ias-box, +.airspeed-alert-trend-underspeed .airspeed-ias-box { + color: yellow; + --airspeed-ias-box-bg: black; + --airspeed-ias-box-scroller-mask-bg: transparent; +} + +.airspeed-ias-box-bg { + width: 100%; + height: 100%; + fill: var(--airspeed-ias-box-fill); + stroke: var(--airspeed-ias-box-stroke); + stroke-width: var(--airspeed-ias-box-border-width); +} + +.airspeed-ias-box-scrollers { + display: flex; + flex-flow: row nowrap; + justify-content: flex-end; + align-items: center; +} + +.airspeed-ias-box-digit-container { + position: relative; + width: calc(33% - 2px); + height: calc(100% - 2 * var(--airspeed-ias-box-border-width)); + margin: 0 1px; + overflow: hidden; +} + +.airspeed-ias-box-digit-container.airspeed-ias-box-hundreds, +.airspeed-ias-box-digit-container.airspeed-ias-box-tens { + height: calc(60% - 2 * var(--airspeed-ias-box-border-width)); +} + +.airspeed-ias-box-digit-container .digit-scroller { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + text-align: center; +} + +.airspeed-ias-box-digit-bg { + position: absolute; + left: 0; + top: 0%; + width: 100%; + height: 100%; + background: var(--airspeed-ias-box-bg); +} + +.airspeed-ias-box-scroller-mask { + position: absolute; + left: -1px; + top: -1px; + right: -1px; + bottom: -1px; + background: var(--airspeed-ias-box-scroller-mask-bg); +} + +.airspeed-tape-color-range { + right: 0%; +} + +.airspeed-tape-color-range-full { + width: var(--airspeed-tape-color-range-full-width); +} + +.airspeed-tape-color-range-half { + width: var(--airspeed-tape-color-range-half-width); +} + +.airspeed-tape-color-range-red { + background: red; +} + +.airspeed-tape-color-range-white { + background: white; +} + +.airspeed-tape-color-range-green { + background: #008000; +} + +.airspeed-tape-color-range-yellow { + background: yellow; +} + +.airspeed-tape-color-range-barber-pole { + background: repeating-linear-gradient(135deg, red, red 6px, white 6px, white 12px); +} + +.airspeed-refspeed-bug-manual { + right: var(--airspeed-refspeed-bug-manual-right); + width: var(--airspeed-refspeed-bug-manual-width); + height: var(--airspeed-refspeed-bug-manual-height); +} + +.airspeed-refspeed-bug-fms { + right: var(--airspeed-refspeed-bug-fms-right); + width: var(--airspeed-refspeed-bug-fms-width); + height: var(--airspeed-refspeed-bug-fms-height); +} + +.airspeed-refspeed-bug-icon { + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 100%; + stroke-width: 1px; +} + +.airspeed-refspeed-bug-icon-manual { + fill: var(--airspeed-refspeed-bug-manual-fill); + stroke: var(--airspeed-refspeed-bug-manual-stroke); +} + +.airspeed-refspeed-bug-icon-fms { + fill: var(--airspeed-refspeed-bug-fms-fill); + stroke: var(--airspeed-refspeed-bug-fms-stroke); +} + +.airspeed-trend { + left: 100%; + width: var(--airspeed-trend-width); + background: magenta; + border: 1px solid white; +} + +/* ---- V-speed bugs ---- */ + +.airspeed-vspeed-fms { + --airspeed-vspeed-bug-icon-color: magenta; +} + +.airspeed-vspeed-bug-icon { + height: var(--airspeed-vspeed-bug-icon-height); + font-size: var(--airspeed-vspeed-bug-icon-font-size); + display: flex; + flex-flow: row nowrap; + align-items: center; + transform: rotateX(0deg); +} + +.airspeed-vspeed-bug-icon-arrow { + height: 100%; + width: 0.5em; + fill: var(--airspeed-vspeed-bug-icon-bg-color); +} + +.airspeed-vspeed-bug-icon-label { + background: var(--airspeed-vspeed-bug-icon-bg-color); + color: var(--airspeed-vspeed-bug-icon-color); + padding-right: 2px; + white-space: pre; +} + +.airspeed-vspeed-bug-container { + left: 100%; + width: calc(100% * var(--airspeed-vspeed-bug-container-width-factor)); +} + +.airspeed-vspeed-bug { + left: var(--airspeed-vspeed-bug-margin-left); +} + +.airspeed-vspeed-offscale-container { + left: 0; + width: calc(100% * (1 + var(--airspeed-vspeed-bug-container-width-factor))); + height: var(--airspeed-vspeed-offscale-container-height); + align-items: stretch; +} + +.airspeed-vspeed-offscale-label { + position: relative; + display: grid; + grid-template-rows: 100%; + grid-template-columns: calc(100% * 1 / (1 + var(--airspeed-vspeed-bug-container-width-factor))) 1fr; + align-items: center; + margin-bottom: var(--airspeed-vspeed-offscale-label-margin-bottom); + + --airspeed-vspeed-offscale-label-color: var(--airspeed-vspeed-bug-icon-color); +} + +.airspeed-vspeed-offscale-label.airspeed-vspeed-fms.airspeed-vspeed-fms-config-miscompare { + --airspeed-vspeed-offscale-label-color: yellow; +} + +.airspeed-vspeed-offscale-label-value { + justify-self: end; + margin-right: var(--airspeed-vspeed-offscale-label-margin-right); + font-size: var(--airspeed-vspeed-offscale-label-font-size); + color: var(--airspeed-vspeed-offscale-label-color); +} + +.airspeed-vspeed-offscale-label .airspeed-vspeed-bug-icon { + justify-self: start; + margin-left: var(--airspeed-vspeed-bug-margin-left); +} + +.airspeed-vspeed-legend-container { + left: 0; + width: calc(100% * var(--airspeed-vspeed-legend-container-width-factor)); + padding: var(--airspeed-vspeed-legend-container-padding); + border-radius: 0 0 0 var(--airspeed-tape-bottomleft-border-radius); + background: black; + align-items: stretch; +} + +.airspeed-vspeed-legend { + position: relative; + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + align-items: baseline; + margin-bottom: var(--airspeed-vspeed-legend-margin-bottom); + font-size: var(--airspeed-vspeed-legend-font-size); + + --airspeed-vspeed-legend-value-color: var(--airspeed-vspeed-bug-icon-color); +} + +.airspeed-vspeed-legend.airspeed-vspeed-fms.airspeed-vspeed-fms-config-miscompare { + --airspeed-vspeed-legend-value-color: yellow; +} + +.airspeed-vspeed-legend-name { + color: var(--airspeed-vspeed-bug-icon-color); +} + +.airspeed-vspeed-legend-value { + color: var(--airspeed-vspeed-legend-value-color); +} + +/* ---- Failed state ---- */ + +.airspeed .failed-box { + position: absolute; + left: 0%; + top: var(--airspeed-top-box-height); + width: 100%; + height: calc(100% - var(--airspeed-top-box-height) - var(--airspeed-bottom-box-height)); +} + +.airspeed.data-failed .failed-box { + display: block; +} + +.airspeed.data-failed .airspeed-tape-label-container { + display: none; +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicator.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicator.tsx new file mode 100644 index 000000000..20ac465ff --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicator.tsx @@ -0,0 +1,77 @@ +import { ArrayUtils, ComponentProps, DisplayComponent, FSComponent, SimVarValueType, Subscribable, VNode } from '@microsoft/msfs-sdk'; + +import { AirspeedIndicator, AirspeedIndicatorDataProvider, VSpeedBugDefinition } from '@microsoft/msfs-garminsdk'; + +import { AirspeedIndicatorConfig } from '../../../../Shared/Profiles/AirspeedIndicator/AirspeedIndicatorConfig'; +import { VSpeedUserSettingManager } from '../../../../Shared/VSpeed/VSpeedUserSettings'; + +import './G1000AirspeedIndicator.css'; + +/** + * Component props for {@link G1000AirspeedIndicator}. + */ +export interface G1000AirspeedIndicatorProps extends ComponentProps { + /** A provider of airspeed indicator data. */ + dataProvider: AirspeedIndicatorDataProvider; + + /** A configuration object defining options for the airspeed indicator. */ + config: AirspeedIndicatorConfig; + + /** A manager for reference V-speed settings. */ + vSpeedSettingManager: VSpeedUserSettingManager; + + /** Whether the indicator should be decluttered. */ + declutter: Subscribable; +} + +/** + * A G3000 airspeed indicator. + */ +export class G1000AirspeedIndicator extends DisplayComponent { + private readonly ref = FSComponent.createRef(); + + /** @inheritdoc */ + public onAfterRender(): void { + this.props.dataProvider.overspeedThreshold.sub(v => { + SimVar.SetGameVarValue('AIRCRAFT_MAXSPEED_OVERRIDE', SimVarValueType.Knots, v - 3); + }, true); + } + + /** @inheritdoc */ + public render(): VNode { + const vSpeedDefs = ArrayUtils.flatMap(Array.from(this.props.vSpeedSettingManager.vSpeedGroups.values()), group => group.vSpeedDefinitions); + const vSpeedBugDefinitions = this.props.config.vSpeedBugConfigs + .map(config => config.resolve()(vSpeedDefs)) + .filter(def => def !== undefined) as VSpeedBugDefinition[]; + + return ( + + ); + } + + /** @inheritdoc */ + public destroy(): void { + this.ref.getOrDefault()?.destroy(); + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicatorDataProvider.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicatorDataProvider.ts new file mode 100644 index 000000000..d927d1a4e --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicatorDataProvider.ts @@ -0,0 +1,19 @@ +import { EventBus } from '@microsoft/msfs-sdk'; + +import { DefaultAirspeedIndicatorDataProvider } from '@microsoft/msfs-garminsdk'; + +import { AirspeedIndicatorConfig } from '../../../../Shared/Profiles/AirspeedIndicator/AirspeedIndicatorConfig'; + +/** + * A G1000 NXi provider of airspeed indicator data. + */ +export class G1000AirspeedIndicatorDataProvider extends DefaultAirspeedIndicatorDataProvider { + /** + * Creates a new instance of G1000AirspeedIndicatorDataProvider. + * @param bus The event bus. + * @param config A configuration object defining options for the airspeed indicator. + */ + public constructor(bus: EventBus, config: AirspeedIndicatorConfig) { + super(bus, 1, config.dataProviderOptions); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/index.ts new file mode 100644 index 000000000..539b2f6c6 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/AirspeedIndicator/index.ts @@ -0,0 +1,2 @@ +export * from './G1000AirspeedIndicator'; +export * from './G1000AirspeedIndicatorDataProvider'; \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/Altimeter.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/Altimeter.tsx index a412e4cce..478026033 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/Altimeter.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/Altimeter.tsx @@ -93,9 +93,9 @@ export class Altimeter extends DisplayComponent { private readonly activeNavSource = ConsumerValue.create(this.props.bus.getSubscriber().on('cdi_select'), { index: 1, type: NavSourceType.Gps }); private readonly vnavTrackingPhase = ConsumerValue.create(this.props.bus.getSubscriber().on('vnav_tracking_phase'), GarminVNavTrackingPhase.None); private readonly vnavTodIndex = ConsumerValue.create(this.props.bus.getSubscriber().on('vnav_tod_global_leg_index'), -1); - private readonly vnavTodDistance = ConsumerValue.create(this.props.bus.getSubscriber().on('vnav_tod_distance'), -1); + private readonly vnavTodDistance = ConsumerSubject.create(this.props.bus.getSubscriber().on('vnav_tod_distance'), -1); private readonly vnavBodDistance = ConsumerValue.create(this.props.bus.getSubscriber().on('vnav_bod_distance'), -1); - private readonly groundSpeed = ConsumerValue.create(this.props.bus.getSubscriber().on('ground_speed').atFrequency(1), 0); + private readonly groundSpeed = ConsumerSubject.create(this.props.bus.getSubscriber().on('ground_speed').atFrequency(1), 0); private vnavState = VNavState.Enabled_Inactive; private vnavConstraintDetails: AltitudeConstraintDetails = { type: AltitudeRestrictionType.Unused, altitude: 0 }; @@ -108,6 +108,15 @@ export class Altimeter extends DisplayComponent { private readonly updateTapeEvent = new SubEvent(); private readonly isGroundLineVisible = Subject.create(false); + private readonly timeToTod = MappedSubject.create( // minutes + ([todDistance, groundSpeed]): number => { + const todDistanceNm = UnitType.METER.convertTo(todDistance, UnitType.NMILE); + return (todDistanceNm / groundSpeed) * 60; + }, + this.vnavTodDistance, this.groundSpeed, + ); + private readonly closeToTod = Subject.create(false); + /** * A callback called after the component renders. @@ -134,9 +143,9 @@ export class Altimeter extends DisplayComponent { .withPrecision(-1) .handle(this.updateVerticalSpeed.bind(this)); sub.on('hEvent').handle(hEvent => { - if (hEvent == 'AS1000_PFD_BARO_INC') { + if ((hEvent == 'AS1000_PFD_BARO_INC') || hEvent == ('AS1000_MFD_BARO_INC')) { this.onbaroKnobTurn(true); - } else if (hEvent == 'AS1000_PFD_BARO_DEC') { + } else if ((hEvent == 'AS1000_PFD_BARO_DEC') || (hEvent == 'AS1000_MFD_BARO_DEC')) { this.onbaroKnobTurn(false); } }); @@ -194,6 +203,9 @@ export class Altimeter extends DisplayComponent { this.vnavState = state; this.manageVnavConstraintAltitudeDisplay(); }); + this.closeToTod.sub((close) => { + if (close) { this.manageVnavConstraintAltitudeDisplay(); } + }, true); this.controller.alerterState.sub(this.onAlerterStateChanged.bind(this)); @@ -202,6 +214,15 @@ export class Altimeter extends DisplayComponent { this.altitudeBugRef.instance.style.display = 'none'; sub.on('adc_state').handle(this.onAdcStateChanged.bind(this)); + + this.timeToTod.sub((t) => { + // true if less than 1 minute from TOD, else false + if (t < 1) { + this.closeToTod.set(true); + } else { + this.closeToTod.set(false); + } + }, true); } /** @@ -283,8 +304,6 @@ export class Altimeter extends DisplayComponent { if (this.vnavTrackingPhase.get() === GarminVNavTrackingPhase.Climb) { showTargetAltitude = this.vnavConstraintDetails.type !== AltitudeRestrictionType.Unused; } else { - const todDistanceNm = UnitType.METER.convertTo(this.vnavTodDistance.get(), UnitType.NMILE); - const PATH_TRACKING_LOOKAHEAD = 1 / 60; if ( // VNAV target altitude is valid this.vnavConstraintDetails.type !== AltitudeRestrictionType.Unused @@ -293,7 +312,7 @@ export class Altimeter extends DisplayComponent { // TOD exists && this.vnavTodIndex.get() >= 0 // Within one minute of TOD - && todDistanceNm / this.groundSpeed.get() < PATH_TRACKING_LOOKAHEAD + && this.closeToTod.get() // Above 250 feet below the VNAV target altitude && this.indicatedAltitudeSub.get().asUnit(UnitType.FOOT) >= this.vnavConstraintDetails.altitude - 250 ) { diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/CAS.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/CAS.css index 64acf2352..8745f4ae2 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/CAS.css +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/CAS.css @@ -5,38 +5,39 @@ width: 148px; height: auto; background-color: black; - border: 1px solid rgb(100,100,100); + border: 1px solid rgb(100, 100, 100); font-size: 16px; - display: none; } #cas>div { margin: 2px 0px 0px 2px; - display: none; } .annunciations-divider { position: relative; width: 90%; height: 0px; - border-bottom: 1px solid rgb(100, 100, 100); + border-bottom: 1px solid rgb(235, 235, 235); left: 5%; display: none; } - .annunciation.warning { + animation: none; color: red; } .annunciation.caution { + animation: none; color: yellow; } .annunciation.advisory { + animation: none; color: white; } .annunciation.safeop { - color: green; + animation: none; + color: limegreen; } \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/CAS.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/CAS.tsx index 57ec9b404..7136eba81 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/CAS.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/CAS.tsx @@ -1,18 +1,42 @@ import { - Annunciation, AnnunciationType, ComponentProps, CompositeLogicXMLHost, DisplayComponent, EventBus, FSComponent, KeyEventData, KeyEvents, KeyEventManager, - VNode, SoundServerController, + Annunciation, AnnunciationType, CasActiveMessage, CasEvents, CasStateEvents, CasSystem, CasSystemLegacyAdapter, ComponentProps, CompositeLogicXMLHost, + DisplayComponent, EventBus, FilteredMappedSubscribableArray, FSComponent, ObjectSubject, SortedMappedSubscribableArray, SoundServerController, VNode } from '@microsoft/msfs-sdk'; +import { CASAlertCounts, CASDisplay } from '@microsoft/msfs-garminsdk'; + import { G1000ControlEvents } from '../../../Shared/G1000Events'; +import { AlertMessage } from '../UI'; import './CAS.css'; -/** The two states an alert can be in. */ -enum AlertState { - /** A newly arrived, unackowledged alert message. */ - New, - /** An alert message that has been acknowledged with the Alert softkey. */ - Acked +/** + * An alerts level on the G1000 NXi. + */ +export enum G1000AlertsLevel { + None, + Advisory, + Caution, + Warning +} + +/** + * CAS events specific to the G1000 NXi. + */ +export interface G1000CasEvents { + /** Synchronizes the current CAS unacknowledged alerts level with the Alerts component. */ + 'cas_unacknowledged_alerts_level': G1000AlertsLevel; +} + +/** + * An alerts message to be associated with a specified CAS. + */ +export interface CasAssociatedMessage { + /** The ID of the CAS alert. */ + casUid: string, + + /** The alert message to display when the CAS alert is active. */ + message: AlertMessage } /** The props for a CAS element. */ @@ -34,315 +58,137 @@ export class CAS extends DisplayComponent { /** The overall container for the CAS elements. */ private divRef = FSComponent.createRef(); /** The div for new, unacked annunciations. */ - private newRef = FSComponent.createRef(); - /** The div for acknowledged but still active annunciations. */ - private ackRef = FSComponent.createRef(); /** The well little div for the divider bar beween acked and unacked. */ private dividerRef = FSComponent.createRef(); private readonly soundController = new SoundServerController(this.props.bus); - - /** The number of unacked messages currently displayed. */ - private numNewDisplayed = 0; - /** The number of acked messages currently displayed. */ - private numAckedDisplayed = 0; - /** The number of warnings we need to be playing a sound for. */ - private numActiveWarnings = 0; - /** The total number of warnings displayed. */ - private totalWarnings = 0; - /** The total number of cautions displayed. */ - private totalCautions = 0; - - /** - * Determine whether we need to hide or unhide ourselves when a child's state changes. - * @param state Whether the alert is acknowledged or not. - * @param type The type of alert. - * @param active Whether the child has gone active. - */ - private setDisplayed(state: AlertState, type: AnnunciationType, active: boolean): void { - switch (state) { - case AlertState.New: - if (active) { - // A new alert has been displayed. - this.numNewDisplayed++; - switch (type) { - case AnnunciationType.Caution: - this.totalCautions++; - this.setMasterStatus(AnnunciationType.Caution, true); - break; - case AnnunciationType.Warning: - this.totalWarnings++; - this.setMasterStatus(AnnunciationType.Warning, true); - break; - } - // If we are adding our first active alert, we need to display the full CAS block. - if (this.numNewDisplayed == 1) { - this.divRef.instance.style.display = 'block'; - } - // If we have any acked messages displayed, we'll show the divider. - if (this.numAckedDisplayed > 0) { - this.dividerRef.instance.style.display = 'block'; - } - // Trigger the play of caution or warning sounds if appropriate. - if (type == AnnunciationType.Caution && this.props.cautionSoundId !== undefined && !this.numActiveWarnings) { - this.soundController.playSound(this.props.cautionSoundId); - } else if (type == AnnunciationType.Warning && this.props.warningSoundId !== undefined && ++this.numActiveWarnings == 1) { - this.soundController.startSound(this.props.warningSoundId); - } - } else { - // A previously displayed alert has been hidden. - this.numNewDisplayed--; - switch (type) { - case AnnunciationType.Caution: - if (--this.totalCautions == 0) { - this.setMasterStatus(AnnunciationType.Caution, false); - } - break; - case AnnunciationType.Warning: - if (--this.totalWarnings == 0) { - this.setMasterStatus(AnnunciationType.Warning, false); - } - } - // If nothing other new alerts are displayed we can hide divider block. - if (this.numNewDisplayed == 0) { - this.dividerRef.instance.style.display = 'none'; - // We'll also go ahead and hide the whole CAS div if there's nothing else displayed. - if (this.numAckedDisplayed == 0) { - this.divRef.instance.style.display = 'none'; - } - } - // Disable the warning sound if it's playing and nothing else needs it. - if (type == AnnunciationType.Warning && this.props.warningSoundId !== undefined && --this.numActiveWarnings == 0) { - this.soundController.stop(this.props.warningSoundId); - } - } - break; - case AlertState.Acked: - if (active) { - // A new acked alert has been displayed. - this.numAckedDisplayed++; - switch (type) { - case AnnunciationType.Caution: - this.totalCautions++; - this.setMasterStatus(AnnunciationType.Caution, true); - break; - case AnnunciationType.Warning: - this.totalWarnings++; - this.setMasterStatus(AnnunciationType.Warning, true); - break; - } - // If we're adding our first acked alert, we need to display the full CAS block. - if (this.numAckedDisplayed == 1) { - this.divRef.instance.style.display = 'block'; - } - if (this.numNewDisplayed > 0) { - // If there are also unacked messages displayed, activate the divider. - this.dividerRef.instance.style.display = 'block'; - } else { - // Otherwise, make sure it's turned off. - this.dividerRef.instance.style.display = 'none'; - } - } else { - // A previously acked alert has been hidden. - this.numAckedDisplayed--; - switch (type) { - case AnnunciationType.Caution: - if (--this.totalCautions == 0) { - this.setMasterStatus(AnnunciationType.Caution, false); - } - break; - case AnnunciationType.Warning: - if (--this.totalWarnings == 0) { - this.setMasterStatus(AnnunciationType.Warning, false); - } - } - // If that was the last one, hide the divider. - if (this.numAckedDisplayed == 0) { - this.dividerRef.instance.style.display = 'none'; - // And if there are also no new ones, hide the full div. - if (this.numNewDisplayed == 0) { - this.divRef.instance.style.display = 'none'; - } - } - } - break; - } - } - - /** Iterate through the configured annunciations and render each into the new and acked divs. */ + private readonly casSystem = new CasSystem(this.props.bus, true); + private readonly casLegacyAdapater = new CasSystemLegacyAdapter(this.props.bus, this.props.logicHandler, this.props.annunciations); + + private readonly newMessages = SortedMappedSubscribableArray.create(FilteredMappedSubscribableArray.create(this.casSystem.casActiveMessageSubject, a => !a.acknowledged), + this.orderMessages.bind(this)); + private readonly ackedMessages = SortedMappedSubscribableArray.create(FilteredMappedSubscribableArray.create(this.casSystem.casActiveMessageSubject, a => a.acknowledged), + this.orderMessages.bind(this)); + + private readonly newMessageCounts = ObjectSubject.create({ + totalAlerts: 0, + countAboveWindow: 0, + countBelowWindow: 0, + numAdvisory: 0, + numCaution: 0, + numWarning: 0, + numSafeOp: 0 + }); + + /** @inheritdoc */ public onAfterRender(): void { - this.setMasterStatus(AnnunciationType.Caution, false); - this.setMasterStatus(AnnunciationType.Warning, false); + this.newMessages.sub(this.onMessagesChanged.bind(this)); + this.ackedMessages.sub(this.onMessagesChanged.bind(this)); - KeyEventManager.getManager(this.props.bus).then(manager => { - manager.interceptKey('MASTER_CAUTION_ACKNOWLEDGE', true); - manager.interceptKey('MASTER_WARNING_ACKNOWLEDGE', true); - }); + this.casLegacyAdapater.start(); - for (const ann of this.props.annunciations) { - FSComponent.render( - , - this.newRef.instance); - FSComponent.render( - , - this.ackRef.instance); - } + this.props.bus.getSubscriber().on('cas_master_caution_active').handle(this.onCautionActive.bind(this)); + this.props.bus.getSubscriber().on('cas_master_warning_active').handle(this.onWarningActive.bind(this)); + this.props.bus.getSubscriber().on('pfd_alert_push').handle(v => v && this.acknowledgeMessages()); + + this.newMessageCounts.sub(this.onMessageCountsChanged.bind(this)); } /** - * Set both sets of simvars relevant to a master caution or warning status. - * @param type The type of the status to set - * @param active Whether or not the status is active + * Handles when unacknowledged message counts have changed. + * @param counts The message counts. */ - private setMasterStatus(type: AnnunciationType, active: boolean): void { - switch (type) { - case AnnunciationType.Caution: - SimVar.SetSimVarValue('K:MASTER_CAUTION_SET', 'bool', active); - SimVar.SetSimVarValue('L:Generic_Master_Caution_Active', 'bool', active); - break; - case AnnunciationType.Warning: - SimVar.SetSimVarValue('K:MASTER_WARNING_SET', 'bool', active); - SimVar.SetSimVarValue('L:Generic_Master_Warning_Active', 'bool', active); - break; + private onMessageCountsChanged(counts: Readonly): void { + let level = G1000AlertsLevel.None; + if (counts.numWarning > 0) { + level = G1000AlertsLevel.Warning; + } else if (counts.numCaution > 0) { + level = G1000AlertsLevel.Caution; + } else if (counts.numAdvisory > 0) { + level = G1000AlertsLevel.Advisory; } + + this.props.bus.getPublisher().pub('cas_unacknowledged_alerts_level', level); } /** - * Render the CAS. - * @returns A VNode. + * Handles when the CAS messages are changed. */ - public render(): VNode { - return ( -
-
-
-
-
) - ; - } -} - -/** The props for a single annunciation. */ -interface AnnunciationProps extends ComponentProps { - /** An event bus. */ - bus: EventBus - /** Our logic handler. */ - logicHandler: CompositeLogicXMLHost, - /** Our annunciation config. */ - config: Annunciation, - /** A subject for passing our state back to the CAS. */ - stateCb: (active: boolean) => void, - /** The state we represent. */ - stateShown: AlertState; -} - -/** An individual annunciation. */ -class CASAnnunciation extends DisplayComponent { - private divRef = FSComponent.createRef(); - /** Whether or not the actual alert condition is currently active. */ - private active = false; - /** Whether or not we are currently showing ourselves. */ - private shown = false; - - /** Show ourselves and let the CAS know, if we're not already shown. */ - private showSelf(): void { - if (!this.shown) { - this.divRef.instance.style.display = 'block'; - this.props.stateCb(true); - this.shown = true; + private onMessagesChanged(): void { + if (this.newMessages.length > 0 && this.ackedMessages.length > 0) { + this.dividerRef.instance.style.display = 'block'; + } else { + this.dividerRef.instance.style.display = 'none'; } - } - /** Hide ourselves and let the CAS know, if we're currently shown. */ - private hideSelf(): void { - if (this.shown) { + if (this.newMessages.length === 0 && this.ackedMessages.length === 0) { this.divRef.instance.style.display = 'none'; - this.props.stateCb(false); - this.shown = false; + } else { + this.divRef.instance.style.display = 'block'; } } - /** Register our alert logic handlers and subscribe to the G1000 bus for alert push events. */ - public onAfterRender(): void { - let intercept: string | undefined; - switch (this.props.config.type) { - case AnnunciationType.Caution: - intercept = 'MASTER_CAUTION_ACKNOWLEDGE'; break; - case AnnunciationType.Warning: - intercept = 'MASTER_WARNING_ACKNOWLEDGE'; break; - } - - if (intercept) { - this.props.bus.getSubscriber().on('key_intercept').handle( - (keyData: KeyEventData) => { - if (keyData.key === intercept) { - this.handleAcknowledgement(); - } - }); - } + /** + * Orders CAS messages as required. + * @param a The first CAS message to compare. + * @param b The second CAS message to compare. + * @returns Negative if b is before a, zero if equal, positive otherwise. + */ + private orderMessages(a: CasActiveMessage, b: CasActiveMessage): number { + return a.priority === b.priority ? b.lastActive - a.lastActive : a.priority - b.priority; + } - const g1000ControlEvents = this.props.bus.getSubscriber(); - g1000ControlEvents.on('pfd_alert_push').handle((evt: boolean) => { - if (evt) { - this.handleAcknowledgement(); - } + /** + * Acknowledges CAS messages. + */ + private acknowledgeMessages(): void { + SimVar.SetSimVarValue('K:MASTER_CAUTION_ACKNOWLEDGE', 'number', 1); + SimVar.SetSimVarValue('K:MASTER_WARNING_ACKNOWLEDGE', 'number', 1); + + const publisher = this.props.bus.getPublisher(); + //Do this on the next frame since the K events will be cached until the end of the frame + //This avoids these being ack'd on this frame and the others the next frame + setTimeout(() => { + publisher.pub('cas_master_acknowledge', AnnunciationType.Advisory, true); + publisher.pub('cas_master_acknowledge', AnnunciationType.SafeOp, true); }); + } - // The composite logic host passes a 1 in the callback if the alert has entered an active state, - // or 0 if it has become inactive. - this.props.logicHandler.addLogicAsNumber(this.props.config.condition, (v: number): void => { - if (v == 1) { - this.active = true; - // We're going active, which means we can't be acked yet. In this case, we only need to - // take action if we show the new alerts; acked alert divs stay idle. - if (this.props.stateShown == AlertState.New) { - this.showSelf(); - } - } else { - this.active = false; - // We're toggling false. Whether we're acked or unacked, we need to hide ourselves and - // let the CAS know. - this.hideSelf(); - } - }, 0); + /** + * Handles when the CAS master caution active state changes. + * @param isActive True if master caution is active, false otherwise. + */ + private onCautionActive(isActive: boolean): void { + if (this.props.cautionSoundId !== undefined && isActive) { + this.soundController.playSound(this.props.cautionSoundId); + } } /** - * Handle a potential acknowledgement event. + * Handles when the CAS master warning active state changes. + * @param isActive True if master warning is active, false otherwise. */ - private handleAcknowledgement(): void { - if (this.props.stateShown == AlertState.New) { - // If we are the new alert, we need to hide ourselves so the acked version can show. - this.hideSelf(); - } else { - // But if we're the acked div, we need to activate ourselves, assuming the alert is hot. - if (this.active) { - this.showSelf(); + private onWarningActive(isActive: boolean): void { + if (this.props.warningSoundId !== undefined) { + if (isActive) { + this.soundController.play({ key: this.props.warningSoundId, sequence: this.props.warningSoundId, continuous: true }); + } else { + this.soundController.stop(this.props.warningSoundId); } } } /** - * Render an annunciation. + * Render the CAS. * @returns A VNode. */ public render(): VNode { - let type: string; - switch (this.props.config.type) { - case AnnunciationType.Advisory: - type = 'advisory'; break; - case AnnunciationType.Caution: - type = 'caution'; break; - case AnnunciationType.SafeOp: - type = 'safeop'; break; - case AnnunciationType.Warning: - type = 'warning'; break; - } - return ( - - ); +
+ +
+ +
) + ; } } \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/index.ts index d3d266ab7..be406b4b6 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/index.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/FlightInstruments/index.ts @@ -1,4 +1,5 @@ export * from './AirspeedIndicator'; + export * from './Altimeter'; export * from './AltitudeAlertController'; export * from './ArtificialHorizon'; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/BottomInfoPanel.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/BottomInfoPanel.css index 968712a0e..cd3c941da 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/BottomInfoPanel.css +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/BottomInfoPanel.css @@ -78,29 +78,28 @@ text-align: right; } -.left-brg-ptr-crs-ident>div { - display: inline-block; -} - .left-brg-ptr-crs { color: magenta; padding-right: 5px; + display: inline-block; } .left-brg-ptr-ident { color: white; padding-right: 5px; - ; + display: inline-block; } .left-brg-ptr-src { font-size: 16px; line-height: 25px; color: cyan; + display: inline-block; } .left-brg-ptr-svg { height: 12px; + display: inline-block; } .right-brg-ptr-container { @@ -128,12 +127,9 @@ text-align: left; } -.right-brg-ptr-crs-ident>div { - display: inline-block; -} - .right-brg-ptr-svg { height: 12px; + display: inline-block; } .right-brg-ptr-src { @@ -142,16 +138,19 @@ text-align: left; line-height: 25px; color: cyan; + display: inline-block; } .right-brg-ptr-ident { color: white; padding-left: 5px; + display: inline-block; } .right-brg-ptr-crs { color: magenta; padding-left: 5px; + display: inline-block; } .bip-time { diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Fma/Fma.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Fma/Fma.tsx index 2fd8cdffa..8a0619176 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Fma/Fma.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Fma/Fma.tsx @@ -215,6 +215,12 @@ export class Fma extends DisplayComponent { return 'GP'; case APVerticalModes.PITCH: return 'PIT'; + case APVerticalModes.LEVEL: + return 'LVL'; + case APVerticalModes.GA: + return 'GA'; + case APVerticalModes.TO: + return 'TO'; case APVerticalModes.CAP: { const alt = this.autopilotModes.verticalAltitudeArmed; return alt === APAltitudeModes.ALTS ? 'ALTS' : alt === APAltitudeModes.ALTV ? 'ALTV' : 'ALT'; @@ -293,6 +299,10 @@ export class Fma extends DisplayComponent { return 'ROL'; case APLateralModes.LEVEL: return 'LVL'; + case APLateralModes.GA: + return 'GA'; + case APLateralModes.TO: + return 'TO'; default: return ''; } @@ -319,6 +329,10 @@ export class Fma extends DisplayComponent { return 'LVL'; case APLateralModes.BC: return 'BC'; + case APLateralModes.GA: + return 'GA'; + case APLateralModes.TO: + return 'TO'; default: return ''; } diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Transponder.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Transponder.css index 084c9720f..4d1fd04ae 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Transponder.css +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Transponder.css @@ -25,6 +25,16 @@ color: rgb(44, 44, 44); } +/* Override green color with white on ground */ +.xpdr-container .on-ground.green { + color: white; + } + +/* Override green highlight with white on ground */ +.xpdr-container .on-ground.highlight-green { + background-color: white; + color: rgb(44, 44, 44); +} /* .highlight { @@ -44,4 +54,4 @@ background-color: none; color: cyan } -} */ \ No newline at end of file +} */ diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Transponder.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Transponder.tsx index 1a9c85efa..f945b05b6 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Transponder.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/Overlays/Transponder.tsx @@ -1,4 +1,6 @@ -import { ComputedSubject, ControlPublisher, DisplayComponent, EventBus, FSComponent, VNode, XPDRMode, XPDRSimVarEvents } from '@microsoft/msfs-sdk'; +import { + AdcEvents, ComputedSubject, ConsumerSubject, ControlPublisher, DisplayComponent, EventBus, FSComponent, HEvent, VNode, XPDRMode, XPDRSimVarEvents, +} from '@microsoft/msfs-sdk'; import { G1000ControlEvents } from '../../../Shared/G1000Events'; @@ -30,7 +32,8 @@ export class Transponder extends DisplayComponent { tempCode: '' }; private readonly xpdrCodeSubject = ComputedSubject.create(0, (v): string => { - return `${Math.round(v)}`.padStart(4, '0'); + const roundedCode = Math.round(v); + return `${isNaN(roundedCode) ? 0 : roundedCode}`.padStart(4, '0'); }); private readonly xpdrModeSubject = ComputedSubject.create(XPDRMode.OFF, (v): string => { switch (v) { @@ -48,6 +51,7 @@ export class Transponder extends DisplayComponent { return 'XXX'; }); + private readonly isOnGround = ConsumerSubject.create(this.props.bus.getSubscriber().on('on_ground').whenChanged(), true); /** * A callback called after the component renders. @@ -68,6 +72,99 @@ export class Transponder extends DisplayComponent { .handle(this.updateCodeEdit.bind(this)); g1000ControlEvents.on('xpdr_code_digit') .handle(this.editCode.bind(this)); + + this.isOnGround.sub(onGround => { + this.xpdrCodeElement.instance.classList.toggle('on-ground', onGround); + this.xpdrModeElement.instance.classList.toggle('on-ground', onGround); + }, true); + + this.props.bus.getSubscriber().on('hEvent').handle(this.handleXpdrHEvent.bind(this)); + } + + /** + * A method to handle XPDR-related Control Pad HEvents. + * @param evt the name of the HEvent. + */ + private handleXpdrHEvent(evt: string): void { + switch (evt) { + case 'AS1000_CONTROL_PAD_Ident': + this.props.controlPublisher.publishEvent('xpdr_send_ident_1', true); + break; + case 'AS1000_PFD_XPDR_Small_INC': + this.changeCodeByOne(true); + break; + case 'AS1000_PFD_XPDR_Small_DEC': + this.changeCodeByOne(false); + break; + case 'AS1000_PFD_XPDR_Large_INC': + this.changeCodeByHundreds(true); + break; + case 'AS1000_PFD_XPDR_Large_DEC': + this.changeCodeByHundreds(false); + break; + } + } + + /** + * Changes the second two digits of the xpdr code by one (tens and ones). + * @param increment whether to increment or decrement the code. + */ + private changeCodeByOne(increment: boolean): void { + // current code will be a rounded number in a padded string, so we parse it as base-8 + const currentCode = parseInt(this.xpdrCodeSubject.get()); + if (isNaN(currentCode)) { + return; + } + let ones = currentCode % 10; + let tens = Math.floor(currentCode / 10) % 10; + ones += (increment ? 1 : -1); + if (ones > 7) { + ones = 0; + tens += 1; + if (tens > 7) { + tens = 0; + } + } + if (ones < 0) { + ones = 7; + tens -= 1; + if (tens < 0) { + tens = 7; + } + } + const newCode = (Math.floor(currentCode / 100) * 100) + tens * 10 + ones; + this.props.controlPublisher.publishEvent('publish_xpdr_code_1', newCode); + } + + /** + * Changes the first two digits of the xpdr code by one (thousands and hundreds). + * @param increment whether to increment or decrement the code. + */ + private changeCodeByHundreds(increment: boolean): void { + // current code will be a rounded number in a padded string, so we parse it as base-8 + const currentCode = parseInt(this.xpdrCodeSubject.get()); + if (isNaN(currentCode)) { + return; + } + let hundreds = Math.floor(currentCode / 100) % 10; + let thousands = Math.floor(currentCode / 1000) % 10; + hundreds += (increment ? 1 : -1); + if (hundreds > 7) { + hundreds = 0; + thousands += 1; + if (thousands > 7) { + thousands = 0; + } + } + if (hundreds < 0) { + hundreds = 7; + thousands -= 1; + if (thousands < 0) { + thousands = 7; + } + } + const newCode = thousands * 1000 + hundreds * 100 + currentCode % 100; + this.props.controlPublisher.publishEvent('publish_xpdr_code_1', newCode); } /** @@ -76,6 +173,7 @@ export class Transponder extends DisplayComponent { */ private updateCodeEdit(edit: boolean): void { if (edit && this.xpdrCodeElement.instance !== null) { + this.codeEdit.charIndex = !this.codeEdit.editMode ? 0 : this.codeEdit.charIndex; this.codeEdit.editMode = true; this.codeEdit.tempCode = ' '; if (this.xpdrModeSubject.getRaw() === XPDRMode.STBY || this.xpdrModeSubject.getRaw() === XPDRMode.OFF) { @@ -87,6 +185,7 @@ export class Transponder extends DisplayComponent { } else if (!edit && this.xpdrCodeElement.instance !== null) { this.codeEdit.editMode = false; this.codeEdit.tempCode = ''; + this.codeEdit.charIndex = 0; this.xpdrCodeElement.instance.classList.remove('highlight-green'); this.xpdrCodeElement.instance.classList.remove('highlight-white'); this.onXpdrModeUpdate(this.xpdrModeSubject.getRaw()); diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/AlertsSubject.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/AlertsSubject.ts index d3b4d474a..41ca9dc22 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/AlertsSubject.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/AlertsSubject.ts @@ -1,4 +1,6 @@ -import { ArraySubject, EventBus, Publisher, SubscribableArray, SubscribableArrayHandler, Subscription } from '@microsoft/msfs-sdk'; +import { + AnnunciationType, ArraySubject, EventBus, Publisher, SortedMappedSubscribableArray, SubscribableArray, SubscribableArrayHandler, Subscription +} from '@microsoft/msfs-sdk'; /** * A message to be displayed in the Alerts pane. @@ -12,6 +14,9 @@ export interface AlertMessage { /** The body of the message. */ message: string; + + /** The optional priority of the message. */ + priority?: AnnunciationType; } /** @@ -34,6 +39,9 @@ export interface AlertMessageEvents { export class AlertsSubject implements SubscribableArray { private readonly data = ArraySubject.create([]); + private readonly timestamps = new Map(); + + private readonly sortedData = SortedMappedSubscribableArray.create(this.data, this.orderAlerts.bind(this), (a, b) => a.key === b.key); private readonly publisher: Publisher; /** @@ -48,6 +56,30 @@ export class AlertsSubject implements SubscribableArray { this.publisher = bus.getPublisher(); } + /** + * Orders the alert messages. + * @param a The first message to order. + * @param b The second message to order. + * @returns Negative if b comes before a, zero if equal, positive if b comes after a. + */ + private orderAlerts(a: AlertMessage, b: AlertMessage): number { + if (a.key === b.key) { + return 0; + } + + const aPriority = a.priority ?? AnnunciationType.SafeOp; + const bPriority = b.priority ?? AnnunciationType.SafeOp; + + if (aPriority === bPriority) { + const aTimestamp = this.timestamps.get(a.key) ?? 0; + const bTimestamp = this.timestamps.get(b.key) ?? 0; + + return bTimestamp - aTimestamp; + } else { + return aPriority - bPriority; + } + } + /** * A callback called when an alert is pushed on the bus. * @param message The alert message that was pushed. @@ -55,6 +87,7 @@ export class AlertsSubject implements SubscribableArray { private onAlertPushed(message: AlertMessage): void { const index = this.data.getArray().findIndex(x => x.key === message.key); if (index < 0) { + this.timestamps.set(message.key, Date.now()); this.data.insert(message, 0); this.publisher.pub('alerts_available', true, false, true); } @@ -68,6 +101,7 @@ export class AlertsSubject implements SubscribableArray { const index = this.data.getArray().findIndex(x => x.key === key); if (index >= 0) { this.data.removeAt(index); + this.timestamps.delete(key); if (this.data.length === 0) { this.publisher.pub('alerts_available', false, false, true); @@ -77,31 +111,31 @@ export class AlertsSubject implements SubscribableArray { /** @inheritdoc */ public get length(): number { - return this.data.length; + return this.sortedData.length; } /** @inheritdoc */ public get(index: number): AlertMessage { - return this.data.get(index); + return this.sortedData.get(index); } /** @inheritdoc */ public tryGet(index: number): AlertMessage | undefined { - return this.data.tryGet(index); + return this.sortedData.tryGet(index); } /** @inheritdoc */ public getArray(): readonly AlertMessage[] { - return this.data.getArray(); + return this.sortedData.getArray(); } /** @inheritdoc */ public sub(handler: SubscribableArrayHandler, initialNotify = false, paused = false): Subscription { - return this.data.sub(handler, initialNotify, paused); + return this.sortedData.sub(handler, initialNotify, paused); } /** @inheritdoc */ public unsub(handler: SubscribableArrayHandler): void { - this.data.unsub(handler); + this.sortedData.unsub(handler); } } \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/CasAlertsBridge.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/CasAlertsBridge.ts new file mode 100644 index 000000000..3045287a8 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/CasAlertsBridge.ts @@ -0,0 +1,74 @@ +import { CasAlertEventData, CasStateEvents, EventBus } from '@microsoft/msfs-sdk'; +import { AlertMessage, AlertMessageEvents } from './AlertsSubject'; + +/** + * CAS events specific to the G1000 NXi. + */ +export interface CasAlertBridgeEvents { + /** Registers an alert message associated with a specified CAS. The alert message key must match the uid of the associated CAS alert. */ + 'cas_register_associated_message': AlertMessage; + + /** Unregisters an alert message associated with a specified CAS. */ + 'cas_unregister_associated_message': string; +} + +/** + * A class for associating alert messages with specific CAS alerts. + */ +export class CasAlertsBridge { + + private readonly publisher = this.bus.getPublisher(); + private readonly associatedAlertMessages = new Map(); + + /** + * Creates an instance of a CasAlertsBridge. + * @param bus The event bus to use with this instance. + */ + constructor(private readonly bus: EventBus) { + const subscriber = this.bus.getSubscriber(); + subscriber.on('cas_register_associated_message').handle(this.registerAssociatedAlertMessage.bind(this)); + subscriber.on('cas_unregister_associated_message').handle(this.unregisterAssociatedAlertMessage.bind(this)); + + subscriber.on('cas_alert_displayed').handle(this.onMessageDisplayed.bind(this)); + subscriber.on('cas_alert_hidden').handle(this.onMessageHidden.bind(this)); + } + + /** + * Handles when a CAS message is displayed. + * @param data The event data about the displayed message. + */ + private onMessageDisplayed(data: CasAlertEventData): void { + const associatedMessage = this.associatedAlertMessages.get(data.uuid); + if (associatedMessage !== undefined) { + associatedMessage.priority = data.priority; + this.publisher.pub('alerts_push', { key: associatedMessage.key, title: associatedMessage.title, message: associatedMessage.message, priority: data.priority }); + } + } + + /** + * Handles when a CAS message is hidden. + * @param data The event data about the hidden message. + */ + private onMessageHidden(data: CasAlertEventData): void { + const associatedMessage = this.associatedAlertMessages.get(data.uuid); + if (associatedMessage !== undefined) { + this.publisher.pub('alerts_remove', associatedMessage.key); + } + } + + /** + * Registers an associated alert message. + * @param alertMessage The alert message to register. + */ + private registerAssociatedAlertMessage(alertMessage: AlertMessage): void { + this.associatedAlertMessages.set(alertMessage.key, alertMessage); + } + + /** + * Unregisters an associated alert message. + * @param uid The ID to unregister. + */ + private unregisterAssociatedAlertMessage(uid: string): void { + this.associatedAlertMessages.delete(uid); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/index.ts index e9e9b3517..ac81ebe71 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/index.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/Alerts/index.ts @@ -1,2 +1,3 @@ export * from './Alerts'; export * from './AlertsSubject'; +export * from './CasAlertsBridge'; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSection.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSection.tsx index cb1ae401a..a6bb61fbf 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSection.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSection.tsx @@ -22,6 +22,8 @@ import { PFDPageMenuDialog } from '../PFDPageMenuDialog'; import { FixInfo } from '../../../../Shared/UI/FPL/FixInfo'; import type { MenuItemDefinition } from '../../../../Shared/UI/Dialogs/PopoutMenuItem'; +import { FmsHEvent } from '../../../../MFD'; +import { ControlpadHEventHandler, ControlPadKeyOperations } from '../../../../Shared/Input/ControlpadHEventHandler'; /** The properties of an FPL detail section item. */ export interface FPLSectionProps extends G1000UiControlProps { /** The view service. */ @@ -728,33 +730,23 @@ export abstract class FPLSection extends G1000UiControl impleme return this.emptyRowRef.getOrDefault()?.isFocused ?? false; } + /** @inheritdoc */ + public consolidateKeyboardHEvent(source: G1000UiControl, evt: FmsHEvent): boolean { + const keyboardInputEvaluation = ControlpadHEventHandler.evaluateKeyboardInput(evt); + if (keyboardInputEvaluation.KeyboardOperation === ControlPadKeyOperations.InsertCharacter) { + this.openWaypointSelectionView(source); + return true; + } + return false; + } + /** * Callback for when UpperKnob event happens on a leg. * @param source The FixInfo element. * @returns True if the control handled the event. */ protected readonly onUpperKnobLegBase = (source: G1000UiControl): boolean => { - let idx = (source instanceof FixInfo) ? this.listRef.instance.indexOf(source) : undefined; - this.props.viewService.open('WptInfo', true) - .onAccept.on((sender, fac: Facility) => { - - if (idx !== undefined) { - if ( - BitFlags.isAll(this.segment.legs[idx]?.flags ?? 0, LegDefinitionFlags.VectorsToFinalFaf) - && BitFlags.isAll(this.segment.legs[idx - 1]?.flags ?? 0, LegDefinitionFlags.VectorsToFinal) - ) { - // If we are inserting before a VTF faf leg with a preceding discontinuity leg, we need to shift the index - // by -1 to ensure we insert the new leg before the discontinuity leg. - idx -= 1; - } - } - - const success = this.props.fms.insertWaypoint(this.segment.segmentIndex, fac, idx); - if (!success) { - this.props.viewService.open('MessageDialog', true).setInput({ inputString: 'Invalid flight plan modification.' }); - } - }); - + this.openWaypointSelectionView(source); return true; }; @@ -815,6 +807,31 @@ export abstract class FPLSection extends G1000UiControl impleme return this.onClrLegBase(sender as FixInfo); }; + /** + * Open the WaypointInfo popup + * @param source Source element + */ + private openWaypointSelectionView(source: G1000UiControl): void { + let idx = (source instanceof FixInfo) ? this.listRef.instance.indexOf(source) : undefined; + this.props.viewService.open('WptInfo', true) + .onAccept.on((sender, fac: Facility) => { + + if (idx !== undefined) { + if (BitFlags.isAll(this.segment.legs[idx]?.flags ?? 0, LegDefinitionFlags.VectorsToFinalFaf) + && BitFlags.isAll(this.segment.legs[idx - 1]?.flags ?? 0, LegDefinitionFlags.VectorsToFinal)) { + // If we are inserting before a VTF faf leg with a preceding discontinuity leg, we need to shift the index + // by -1 to ensure we insert the new leg before the discontinuity leg. + idx -= 1; + } + } + + const success = this.props.fms.insertWaypoint(this.segment.segmentIndex, fac, idx); + if (!success) { + this.props.viewService.open('MessageDialog', true).setInput({ inputString: 'Invalid flight plan modification.' }); + } + }); + } + /** * A callback which is called when a leg selection changes. * @param item The selected item. @@ -986,7 +1003,9 @@ export abstract class FPLSection extends G1000UiControl impleme ref={this.listRef} data={this.legs} renderItem={this.renderItem} onItemSelected={this.onLegItemSelected.bind(this)} - hideScrollbar scrollContainer={this.props.scrollContainer} + hideScrollbar + scrollToMostRecentlyAdded + scrollContainer={this.props.scrollContainer} reconcileChildBlur={(): BlurReconciliation => BlurReconciliation.Next} requireChildFocus /> diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSectionDeparture.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSectionDeparture.tsx index 8f4e58f6a..23241288f 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSectionDeparture.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSectionDeparture.tsx @@ -10,11 +10,59 @@ import { SetRunway } from '../../../../Shared/UI/SetRunway/SetRunway'; import { WptInfo } from '../../../../Shared/UI/WptInfo/WptInfo'; import { FixInfo } from '../../../../Shared/UI/FPL/FixInfo'; import { FPLSection } from './FPLSection'; +import { ControlpadHEventHandler, ControlPadKeyOperations } from '../../../../Shared/Input/ControlpadHEventHandler'; +import { FmsHEvent } from '../../../../MFD'; /** * Render the departure phase of the flight plan. */ export class FPLDeparture extends FPLSection { + /** @inheritdoc */ + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + } + + /** @inheritdoc */ + public consolidateKeyboardHEvent(source: G1000UiControl, evt: FmsHEvent): boolean { + const keyboardInputEvaluation = ControlpadHEventHandler.evaluateKeyboardInput(evt); + if (keyboardInputEvaluation.KeyboardOperation === ControlPadKeyOperations.InsertCharacter) { + const plan = this.props.fms.getFlightPlan(); + const origin = plan.originAirport; + if (!origin || origin === undefined) { + this.openDepartureSelectionView(); + return true; + } + } + return false; + } + + /** Open the WaypointInfo popup */ + private openDepartureSelectionView(): void { + this.props.viewService.open('WptInfo', true) + .onAccept.on((sender, fac) => { + if (fac === undefined) { + return; + } + + // check if its airportfacility interface + if ('bestApproach' in fac) { + this.props.fms.setOrigin(fac as AirportFacility); + this.props.viewService.open('SetRunway', true).setInput(fac as AirportFacility) + .onAccept.on((subSender, data) => { + this.props.fms.setOrigin(this.props.facilities.originFacility, data); + }); + } else { + const firstEnrSegment = this.props.fms.getFlightPlan().segmentsOfType(FlightPlanSegmentType.Enroute).next().value; + if (firstEnrSegment) { + const success = this.props.fms.insertWaypoint(firstEnrSegment.segmentIndex, fac, 0); + if (!success) { + this.props.viewService.open('MessageDialog', true).setInput({ inputString: 'Invalid flight plan modification.' }); + } + } + } + }); + } + /** @inheritdoc */ protected getEmptyRowVisbility(): boolean { const plan = this.props.fms.getFlightPlan(0); @@ -54,33 +102,9 @@ export class FPLDeparture extends FPLSection { const plan = this.props.fms.getFlightPlan(); const origin = plan.originAirport; if (!origin || origin === undefined) { - this.props.viewService.open('WptInfo', true) - .onAccept.on((sender, fac) => { - if (fac === undefined) { - return; - } - - // check if its airportfacility interface - if ('bestApproach' in fac) { - this.props.fms.setOrigin(fac as AirportFacility); - this.props.viewService.open('SetRunway', true).setInput(fac as AirportFacility) - .onAccept.on((subSender, data) => { - this.props.fms.setOrigin(this.props.facilities.originFacility, data); - }); - } else { - const firstEnrSegment = this.props.fms.getFlightPlan().segmentsOfType(FlightPlanSegmentType.Enroute).next().value; - if (firstEnrSegment) { - const success = this.props.fms.insertWaypoint(firstEnrSegment.segmentIndex, fac, 0); - if (!success) { - this.props.viewService.open('MessageDialog', true).setInput({ inputString: 'Invalid flight plan modification.' }); - } - } - } - }); - + this.openDepartureSelectionView(); return true; } - return false; }; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSectionDestination.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSectionDestination.tsx index b95e3201d..a1b729880 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSectionDestination.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/FPL/FPLSectionDestination.tsx @@ -6,6 +6,8 @@ import { G1000UiControl } from '../../../../Shared/UI/G1000UiControl'; import { SetRunway } from '../../../../Shared/UI/SetRunway/SetRunway'; import { FixInfo } from '../../../../Shared/UI/FPL/FixInfo'; import { FPLSection } from './FPLSection'; +import { FmsHEvent } from '../../../../MFD'; +import { ControlpadHEventHandler, ControlPadKeyOperations } from '../../../../Shared/Input/ControlpadHEventHandler'; /** * Render the destination info for a flight plan. @@ -20,6 +22,20 @@ export class FPLDestination extends FPLSection { return noAppArr && !hasRunway && (!destination || destination == ''); } + /** @inheritdoc */ + public consolidateKeyboardHEvent(source: G1000UiControl, evt: FmsHEvent): boolean { + const keyboardInputEvaluation = ControlpadHEventHandler.evaluateKeyboardInput(evt); + if (keyboardInputEvaluation.KeyboardOperation === ControlPadKeyOperations.InsertCharacter) { + const plan = this.props.fms.getFlightPlan(); + const destination = plan.destinationAirport; + if (!destination || destination === undefined) { + this.openDestinationSelectionView(); + return true; + } + } + return false; + } + /** @inheritdoc */ protected onUpperKnobLegBase = (source: G1000UiControl): boolean => { const legIndex = this.listRef.instance.indexOf(source); @@ -43,16 +59,8 @@ export class FPLDestination extends FPLSection { const plan = this.props.fms.getFlightPlan(); const destination = plan.destinationAirport; if (!destination || destination === undefined) { - // EMPTY ROW - this.props.viewService.open('WptInfo', true) - .onAccept.on((sender, fac: AirportFacility) => { - this.props.fms.setDestination(fac as AirportFacility); - this.props.viewService.open('SetRunway', true).setInput(fac as AirportFacility).onAccept.on((subSender, data) => { - this.props.fms.setDestination(this.props.facilities.destinationFacility, data); - }); - }); + this.openDestinationSelectionView(); } - return true; }; @@ -74,6 +82,19 @@ export class FPLDestination extends FPLSection { return false; }; + /** Open the WaypointInfo popup */ + private openDestinationSelectionView(): void { + // EMPTY ROW + this.props.viewService.open('WptInfo', true) + .onAccept.on((sender, fac: AirportFacility) => { + this.props.fms.setDestination(fac as AirportFacility); + this.props.viewService.open('SetRunway', true).setInput(fac as AirportFacility).onAccept.on((subSender, data) => { + this.props.fms.setDestination(this.props.facilities.destinationFacility, data); + }); + }); + + } + /** @inheritdoc */ protected onHeaderFocused(): void { super.onHeaderFocused(); diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/PFDViewService.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/PFDViewService.ts index b1066053a..7e425eba6 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/PFDViewService.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/PFDViewService.ts @@ -29,7 +29,7 @@ export class PFDViewService extends ViewService { ['AS1000_PFD_FPL_Push', FmsHEvent.FPL], ['AS1000_PFD_PROC_Push', FmsHEvent.PROC], ['AS1000_PFD_RANGE_INC', FmsHEvent.RANGE_INC], - ['AS1000_PFD_RANGE_DEC', FmsHEvent.RANGE_DEC] + ['AS1000_PFD_RANGE_DEC', FmsHEvent.RANGE_DEC], ]); /** @@ -38,6 +38,7 @@ export class PFDViewService extends ViewService { */ constructor(readonly bus: EventBus) { super(bus); + const g1000Pub = this.bus.getSubscriber(); g1000Pub.on('pfd_timerref_push').handle(() => { this.onInteractionEvent('pfd_timerref_push'); @@ -53,11 +54,8 @@ export class PFDViewService extends ViewService { }); } - /** - * Routes the HEvents to the views. - * @param hEvent The event identifier. - */ - protected onInteractionEvent(hEvent: string): void { + /** @inheritdoc */ + protected onInteractionEvent(hEvent: string): boolean { // Handling a few special cases here to keep the other stuff nice ;) const activeView = this.activeView.get(); @@ -66,19 +64,17 @@ export class PFDViewService extends ViewService { if (hEvent === 'AS1000_PFD_MENU_Push') { if (!activeView) { this.open(PFDSetup.name); - return; + return true; } else if (activeView instanceof PFDSetup) { activeView.close(); - return; + return true; } } - const evt = this.fmsEventMap.get(hEvent); - if (evt !== undefined && this.routeInteractionEventToViews(evt)) { - return; + if (super.onInteractionEvent(hEvent)) { + return true; } - switch (hEvent) { // TODO move these events out in the next iteration, since we dont want type refs to the views in here case 'AS1000_PFD_FPL_Push': @@ -127,5 +123,6 @@ export class PFDViewService extends ViewService { this.open('DirectTo'); break; } + return true; } } \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/TimerRef/TimerRef.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/TimerRef/TimerRef.css index 3ca2842d9..8f7bba92a 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/TimerRef/TimerRef.css +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/TimerRef/TimerRef.css @@ -38,78 +38,74 @@ .timerref-hr1 { position: absolute; - top: 30px; left: -3px; + top: 30px; width: 99%; border: 1px solid rgb(100, 100, 100); } -.timerref-glide-title { +.timerref-ref-container { position: absolute; + left: 0px; top: 35px; + width: 100%; + height: 80px; } -.timerref-glide-value, -.timerref-vr-value, -.timerref-vx-value, -.timerref-vy-value { +.timerref-ref-list { position: absolute; - top: 35px; - left: 146px; - width: 60px; - color: cyan; - text-align: right; + left: 0px; + top: 0px; + width: calc(100% - 4px); + height: 100%; } -.timerref-glide-toggle { - position: absolute; - right: 20px; - top: 35px; -} - -.timerref-vr-title { - position: absolute; - top: 55px; -} - -.timerref-vr-value { - top: 55px; +.timerref-ref-row { + position: relative; + height: 20px; + display: flex; + flex-flow: row nowrap; } -.timerref-vr-toggle { - position: absolute; - right: 20px; - top: 55px; +.timerref-ref-header { + justify-content: center; + align-items: center; + font-size: 15px; } -.timerref-vx-title { - position: absolute; - top: 75px; +.timerref-ref-entry { + justify-content: flex-start; + align-items: baseline; } -.timerref-vx-value { - top: 75px; +.timerref-ref-title { + flex-shrink: 0; + width: 146px; + font-size: 16px; } -.timerref-vx-toggle { - position: absolute; - right: 20px; - top: 75px; +.timerref-ref-value { + flex-shrink: 0; + width: 60px; + color: cyan; + text-align: right; } -.timerref-vy-title { - position: absolute; - top: 95px; +.timerref-ref-unit { + font-size: 14px; } -.timerref-vy-value { - top: 95px; +.timerref-ref-asterisk { + color: #fff; + width: 9px; + display: inline-block; } -.timerref-vy-toggle { - position: absolute; - right: 20px; - top: 95px; +.timerref-ref-toggle { + flex-grow: 1; + max-width: 78px; + justify-content: flex-end; + font-size: 16px; } .timerref-hr2 { @@ -195,10 +191,4 @@ left: 4px; height: 50px; padding: 5px; -} - -.timerref-asterisk { - color: #fff; - width: 9px; - display: inline-block; -} +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/TimerRef/TimerRef.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/TimerRef/TimerRef.tsx index bd92434f3..44a59f309 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/TimerRef/TimerRef.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/Components/UI/TimerRef/TimerRef.tsx @@ -1,18 +1,25 @@ import { - ArraySubject, ComputedSubject, EventBus, FSComponent, MinimumsControlEvents, MinimumsEvents, MinimumsMode, NumberUnitSubject, Subject, Unit, UnitFamily, - UnitType, VNode + ArraySubject, ArrayUtils, ComputedSubject, EventBus, FSComponent, MappedSubject, MappedSubscribable, MinimumsControlEvents, MinimumsEvents, + MinimumsMode, NumberUnitSubject, Subject, Unit, UnitFamily, UnitType, UserSetting, VNode } from '@microsoft/msfs-sdk'; -import { G1000ControlEvents } from '../../../../Shared/G1000Events'; +import { VSpeedUserSettingUtils } from '@microsoft/msfs-garminsdk'; + import { ContextMenuDialog, ContextMenuItemDefinition } from '../../../../Shared/UI/Dialogs/ContextMenuDialog'; +import { MenuItemDefinition } from '../../../../Shared/UI/Dialogs/PopoutMenuItem'; import { FmsHEvent } from '../../../../Shared/UI/FmsHEvent'; +import { G1000ControlList, G1000UiControl } from '../../../../Shared/UI/G1000UiControl'; import { ActionButton } from '../../../../Shared/UI/UIControls/ActionButton'; import { ArrowToggle } from '../../../../Shared/UI/UIControls/ArrowToggle'; import { NumberInput } from '../../../../Shared/UI/UIControls/NumberInput'; import { SelectControl } from '../../../../Shared/UI/UIControls/SelectControl'; +import { ArrowControl } from '../../../../Shared/UI/UiControls2/ArrowControl'; +import { DigitInput } from '../../../../Shared/UI/UiControls2/DigitInput'; +import { G1000UiControlWrapper } from '../../../../Shared/UI/UiControls2/G1000UiControlWrapper'; import { UiView, UiViewProps } from '../../../../Shared/UI/UiView'; import { UnitsUserSettingManager } from '../../../../Shared/Units/UnitsUserSettings'; -import { VSpeed, VSpeedType } from '../../FlightInstruments/AirspeedIndicator'; +import { VSpeedDefinition, VSpeedGroup } from '../../../../Shared/VSpeed/VSpeed'; +import { VSpeedUserSettingManager } from '../../../../Shared/VSpeed/VSpeedUserSettings'; import { PFDPageMenuDialog } from '../PFDPageMenuDialog'; import { Timer, TimerMode } from './Timer'; import { TimerInput } from './TimerInput'; @@ -25,37 +32,139 @@ import './TimerRef.css'; interface TimerRefProps extends UiViewProps { /** An instance of the event bus. */ bus: EventBus; + + /** A manager for reference V-speed user settings. */ + vSpeedSettingManager: VSpeedUserSettingManager; + /** A user setting manager. */ unitsSettingManager: UnitsUserSettingManager; + /** Whether this instance of the G1000 has a Radio Altimeter. */ hasRadioAltimeter: boolean; } +/** + * An entry describing a reference V-speed that can be edited through the TimerRef menu. + */ +type VSpeedEntry = { + /** The V-speed definition. */ + definition: VSpeedDefinition; + + /** The user setting controlling whether the V-speed bug is shown on the airspeed indicator. */ + showSetting: UserSetting; + + /** The user setting controlling the default value of the V-speed. */ + defaultValueSetting: UserSetting; + + /** The user setting controlling the user-defined value of the V-speed. */ + userValueSetting: UserSetting; + + /** The current active value of the V-speed. */ + activeValue: MappedSubscribable; + + /** Whether the active value of the V-speed differs from the default value. */ + isEdited: MappedSubscribable; +}; + /** * The PFD timer ref popout. */ export class TimerRef extends UiView { + private static readonly VSPEED_GROUP_COMPARATOR = (a: VSpeedGroup, b: VSpeedGroup): number => { + // Default group ('') goes before all other groups. + if (a.name === '') { + return -1; + } else if (b.name === '') { + return 1; + } else { + return 0; + } + }; + public popoutRef = FSComponent.createRef(); private readonly containerRef = FSComponent.createRef(); private readonly minsToggleComponent = FSComponent.createRef(); private readonly minsInputComponent = FSComponent.createRef(); - private readonly glideRef = Subject.create(1); - private readonly glideRefChanged = ComputedSubject.create(false, (v) => { return v ? ' *' : ''; }); - private readonly vrRef = Subject.create(1); - private readonly vrRefChanged = ComputedSubject.create(false, (v) => { return v ? ' *' : ''; }); - private readonly vxRef = Subject.create(1); - private readonly vxRefChanged = ComputedSubject.create(false, (v) => { return v ? ' *' : ''; }); - private readonly vyRef = Subject.create(1); - private readonly vyRefChanged = ComputedSubject.create(false, (v) => { return v ? ' *' : ''; }); + private readonly vSpeedGroups = Array.from(this.props.vSpeedSettingManager.vSpeedGroups.values()).sort(TimerRef.VSPEED_GROUP_COMPARATOR); + + private readonly vSpeedRowData = ArraySubject.create( + ArrayUtils.flatMap(this.vSpeedGroups, group => { + return group.name === '' ? group.vSpeedDefinitions : [group.name, ...group.vSpeedDefinitions]; + }).map(row => { + if (typeof row === 'string') { + return row; + } + + const definition = row; + const activeValue = VSpeedUserSettingUtils.activeValue(definition.name, this.props.vSpeedSettingManager, false, true); + + return { + definition, + showSetting: this.props.vSpeedSettingManager.getSetting(`vSpeedShow_${definition.name}`), + defaultValueSetting: this.props.vSpeedSettingManager.getSetting(`vSpeedDefaultValue_${definition.name}`), + userValueSetting: this.props.vSpeedSettingManager.getSetting(`vSpeedUserValue_${definition.name}`), + activeValue, + isEdited: MappedSubject.create( + ([activeVal, defaultVal]) => activeVal !== defaultVal, + activeValue, + this.props.vSpeedSettingManager.getSetting(`vSpeedDefaultValue_${definition.name}`) + ) + } as VSpeedEntry; + }) + ); + + private readonly menuItems: MenuItemDefinition[] = [ + { + id: 'enable-all', + renderContent: (): VNode => All References On, + isEnabled: true, + action: this.setShowVSpeedBugs.bind(this, true, undefined) + }, + { + id: 'disable-all', + renderContent: (): VNode => All References Off, + isEnabled: true, + action: this.setShowVSpeedBugs.bind(this, false, undefined) + }, + { + id: 'restore-defaults', + renderContent: (): VNode => Restore Defaults, + isEnabled: true, + action: this.resetVSpeeds.bind(this, undefined) + }, + ...ArrayUtils.flatMap(this.vSpeedGroups.filter(group => group.name !== ''), group => { + return [ + { + id: `enable-all-${group.name}`, + renderContent: (): VNode => {group.name} References On, + isEnabled: true, + action: this.setShowVSpeedBugs.bind(this, true, group.name) + }, + { + id: `disable-all-${group.name}`, + renderContent: (): VNode => {group.name} References Off, + isEnabled: true, + action: this.setShowVSpeedBugs.bind(this, false, group.name) + }, + { + id: `restore-defaults-${group.name}`, + renderContent: (): VNode => Restore {group.name} Defaults, + isEnabled: true, + action: this.resetVSpeeds.bind(this, group.name) + } + ]; + }) + ]; + private readonly minsRef = Subject.create(0); private readonly timerComponentRef = FSComponent.createRef(); private readonly upDownItems = ArraySubject.create(); private readonly buttonRef = FSComponent.createRef(); private readonly upDownControlRef = FSComponent.createRef>(); private timerButtonSubject = Subject.create('Start?'); - private g1000Pub = this.props.bus.getPublisher(); + private controlPub = this.props.bus.getPublisher(); /** @@ -78,22 +187,6 @@ export class TimerRef extends UiView { public timer = new Timer(this.props.bus, this.onTimerModeChanged, this.onTimerValueChanged); - private vSpeeds: VSpeed[] = [ - { type: VSpeedType.Vx, value: Math.round(Simplane.getDesignSpeeds().Vx), modified: Subject.create(false), display: true }, - { type: VSpeedType.Vy, value: Math.round(Simplane.getDesignSpeeds().Vy), modified: Subject.create(false), display: true }, - { type: VSpeedType.Vr, value: Math.round(Simplane.getDesignSpeeds().Vr), modified: Subject.create(false), display: true }, - { type: VSpeedType.Vglide, value: Math.round(Simplane.getDesignSpeeds().BestGlide), modified: Subject.create(false), display: true }, - { type: VSpeedType.Vapp, value: Math.round(Simplane.getDesignSpeeds().Vapp), modified: Subject.create(false), display: false } - ]; - private vSpeedSubjects = { - vx: Subject.create(this.vSpeeds[0].value), - vy: Subject.create(this.vSpeeds[1].value), - vr: Subject.create(this.vSpeeds[2].value), - vg: Subject.create(this.vSpeeds[3].value), - vapp: Subject.create(this.vSpeeds[4].value) - }; - - // minimums private readonly minimumsSubscriber = this.props.bus.getSubscriber(); private readonly decisionHeight = NumberUnitSubject.create(UnitType.FOOT.createNumber(0)); @@ -104,13 +197,7 @@ export class TimerRef extends UiView { ); private minsToggleOptions = ['Off', 'BARO', 'RA', 'TEMP COMP']; - private vSpeedToggleMap: Map = new Map(); - private vSpeedSubjectMap: Map> = new Map(); - private vSpeedObjectMap: Map = new Map(); - - private onOffToggleOptions = ['Off', 'On']; - - /** @inheritdoc */ + /** @inheritDoc */ public onInteractionEvent(evt: FmsHEvent): boolean { switch (evt) { case FmsHEvent.CLR: @@ -130,38 +217,12 @@ export class TimerRef extends UiView { public onMenu(): boolean { // console.log('called menu'); const dialog = this.props.viewService.open('PageMenuDialog', true) as PFDPageMenuDialog; - dialog.setMenuItems([ - { - id: 'enable-all', - renderContent: (): VNode => All References On, - isEnabled: true, - action: (): void => { - this.enableAllRefSpeeds(true); - } - }, - { - id: 'disable-all', - renderContent: (): VNode => All References Off, - isEnabled: true, - action: (): void => { - this.enableAllRefSpeeds(false); - } - }, - { - id: 'restore-defaults', - renderContent: (): VNode => Restore Defaults, - isEnabled: true, - action: (): void => { - // console.log('Restore defaults'); - this.resetVSpeeds(); - } - }, - ]); + dialog.setMenuItems(this.menuItems); return true; } - /** @inheritdoc */ + /** @inheritDoc */ public onAfterRender(node: VNode): void { super.onAfterRender(node); @@ -173,23 +234,6 @@ export class TimerRef extends UiView { this.minsToggleComponent.instance.props.options = this.minsToggleOptions; this.upDownItems.set(['Up', 'Dn']); - this.vSpeedToggleMap.set(3, this.vSpeeds[3]); - this.vSpeedToggleMap.set(5, this.vSpeeds[2]); - this.vSpeedToggleMap.set(7, this.vSpeeds[0]); - this.vSpeedToggleMap.set(9, this.vSpeeds[1]); - this.vSpeedSubjectMap.set(VSpeedType.Vglide, this.vSpeedSubjects.vg); - this.vSpeedSubjectMap.set(VSpeedType.Vr, this.vSpeedSubjects.vr); - this.vSpeedSubjectMap.set(VSpeedType.Vx, this.vSpeedSubjects.vx); - this.vSpeedSubjectMap.set(VSpeedType.Vy, this.vSpeedSubjects.vy); - this.vSpeedObjectMap.set(VSpeedType.Vglide, this.vSpeeds[3]); - this.vSpeedObjectMap.set(VSpeedType.Vr, this.vSpeeds[2]); - this.vSpeedObjectMap.set(VSpeedType.Vx, this.vSpeeds[0]); - this.vSpeedObjectMap.set(VSpeedType.Vy, this.vSpeeds[1]); - - this.vSpeeds[0].modified.sub(v => this.vxRefChanged.set(v)); - this.vSpeeds[1].modified.sub(v => this.vyRefChanged.set(v)); - this.vSpeeds[2].modified.sub(v => this.vrRefChanged.set(v)); - this.vSpeeds[3].modified.sub(v => this.glideRefChanged.set(v)); this.minimumsUnit.set(this.props.unitsSettingManager.altitudeUnits.get()); @@ -235,51 +279,42 @@ export class TimerRef extends UiView { } }; - /** Method to reset all v speeds to defaults */ - private resetVSpeeds(): void { - this.vSpeeds[0].value = Math.round(Simplane.getDesignSpeeds().Vx); - this.vSpeedSubjects.vx.set(this.vSpeeds[0].value); - this.vSpeeds[0].modified.set(false); - this.g1000Pub.pub('vspeed_set', this.vSpeeds[0], true); - this.vSpeeds[1].value = Math.round(Simplane.getDesignSpeeds().Vy); - this.vSpeedSubjects.vy.set(this.vSpeeds[1].value); - this.vSpeeds[1].modified.set(false); - this.g1000Pub.pub('vspeed_set', this.vSpeeds[1], true); - this.vSpeeds[2].value = Math.round(Simplane.getDesignSpeeds().Vr); - this.vSpeedSubjects.vr.set(this.vSpeeds[2].value); - this.vSpeeds[2].modified.set(false); - this.g1000Pub.pub('vspeed_set', this.vSpeeds[2], true); - this.vSpeeds[3].value = Math.round(Simplane.getDesignSpeeds().BestGlide); - this.vSpeedSubjects.vg.set(this.vSpeeds[3].value); - this.vSpeeds[3].modified.set(false); - this.g1000Pub.pub('vspeed_set', this.vSpeeds[3], true); - this.vSpeeds[4].value = Math.round(Simplane.getDesignSpeeds().Vapp); - this.vSpeeds[4].modified.set(false); - } - /** - * Method enable or disable all ref speeds. - * @param enable Whether to enable or disable the ref speeds. + * Resets V-speeds to their default values. + * @param groupName The name of the V-speed group containing the V-speeds to reset. If not defined, then the change + * will be applied to all V-speeds in every group. */ - private enableAllRefSpeeds(enable: boolean): void { - const value = enable ? 1 : 0; - this.onVyRefOptionSelected(value); - this.onVxRefOptionSelected(value); - this.onVrRefOptionSelected(value); - this.onGlideRefOptionSelected(value); + private resetVSpeeds(groupName?: string): void { + let currentGroup = ''; + for (const row of this.vSpeedRowData.getArray()) { + if (typeof row === 'string') { + currentGroup = row; + continue; + } + + if (groupName === undefined || groupName === currentGroup) { + row.userValueSetting.value = -1; + } + } } /** - * Method to set vspeed asterisk visibility. - * @param vspeed is the VSpeedType to be updated - * @param value is the vspeed value + * Sets whether to show V-speed bugs. + * @param show Whether to show the bugs. + * @param groupName The name of the V-speed group containing the V-speeds to show or hide. If not defined, then the + * change will be applied to all V-speeds in every group. */ - private updateVSpeed(vspeed: VSpeedType, value: number): void { - const object = this.vSpeedObjectMap.get(vspeed); - if (object !== undefined) { - object.value = value; - object.modified.set(true); - this.g1000Pub.pub('vspeed_set', object, true); + private setShowVSpeedBugs(show: boolean, groupName?: string): void { + let currentGroup = ''; + for (const row of this.vSpeedRowData.getArray()) { + if (typeof row === 'string') { + currentGroup = row; + continue; + } + + if (groupName === undefined || groupName === currentGroup) { + row.showSetting.value = show; + } } } @@ -299,51 +334,6 @@ export class TimerRef extends UiView { } }; - // ---- TOGGLE Vg CALLBACK - private onGlideRefOptionSelected = (index: number): void => { - // console.log('INDEX HERE -- ', index); - this.glideRef.set(index); - const vSpeed = this.vSpeedObjectMap.get(VSpeedType.Vglide); - if (vSpeed !== undefined) { - vSpeed.value = this.vSpeedSubjects.vg.get(); - vSpeed.display = index === 1; - this.g1000Pub.pub('vspeed_display', vSpeed, true); - } - }; - - // ---- TOGGLE Vr CALLBACK - private onVrRefOptionSelected = (index: number): void => { - this.vrRef.set(index); - const vSpeed = this.vSpeedObjectMap.get(VSpeedType.Vr); - if (vSpeed !== undefined) { - vSpeed.value = this.vSpeedSubjects.vr.get(); - vSpeed.display = index === 1; - this.g1000Pub.pub('vspeed_display', vSpeed, true); - } - }; - - // ---- TOGGLE Vx CALLBACK - private onVxRefOptionSelected = (index: number): void => { - this.vxRef.set(index); - const vSpeed = this.vSpeedObjectMap.get(VSpeedType.Vx); - if (vSpeed !== undefined) { - vSpeed.value = this.vSpeedSubjects.vx.get(); - vSpeed.display = index === 1; - this.g1000Pub.pub('vspeed_display', vSpeed, true); - } - }; - - // ---- TOGGLE Vy CALLBACK - private onVyRefOptionSelected = (index: number): void => { - this.vyRef.set(index); - const vSpeed = this.vSpeedObjectMap.get(VSpeedType.Vy); - if (vSpeed !== undefined) { - vSpeed.value = this.vSpeedSubjects.vy.get(); - vSpeed.display = index === 1; - this.g1000Pub.pub('vspeed_display', vSpeed, true); - } - }; - // ---- TOGGLE MINIMUMS CALLBACK private onMinimumsRefOptionSelected = (index: number): void => { this.minsRef.set(index); @@ -378,30 +368,7 @@ export class TimerRef extends UiView { return { id: index.toString(), renderContent: (): VNode => {item}, estimatedWidth: item.length * ContextMenuDialog.CHAR_WIDTH }; }; - // ---- updateVy Callback method - private updateVy = (value: number): void => { - this.updateVSpeed(VSpeedType.Vy, value); - }; - - // ---- updateVy Callback method - private updateVx = (value: number): void => { - this.updateVSpeed(VSpeedType.Vx, value); - }; - - // ---- updateVy Callback method - private updateVr = (value: number): void => { - this.updateVSpeed(VSpeedType.Vr, value); - }; - - // ---- updateVy Callback method - private updateVglide = (value: number): void => { - this.updateVSpeed(VSpeedType.Vglide, value); - }; - - /** - * Renders the component. - * @returns The component VNode. - */ + /** @inheritDoc */ public render(): VNode { return (
@@ -410,32 +377,64 @@ export class TimerRef extends UiView { +
-
GLIDE
-
- - KT{this.glideRefChanged} +
+ + { + if (typeof row === 'string') { + return ( + +
{row}
+
+ ); + } else { + const entry = row; + const inputValue = Subject.create(0); + + entry.activeValue.pipe(inputValue); + + MappedSubject.create( + ([inputVal, defaultVal]) => inputVal === defaultVal ? -1 : inputVal, + inputValue, + entry.defaultValueSetting + ).pipe(entry.userValueSetting); + + return ( + +
+
{entry.definition.label}
+
+ + KT{entry.isEdited.map(isEdited => isEdited ? '*' : '')} +
+ value ? 'On' : 'Off'} + class='timerref-ref-toggle' + /> +
+
+ ); + } + }} + hideScrollbar={this.vSpeedRowData.length <= 4} + class='timerref-ref-list' + /> +
- -
Vr
-
- - KT{this.vrRefChanged} -
- -
Vx
-
- - KT{this.vxRefChanged} -
- -
Vy
-
- - KT{this.vyRefChanged} -
-
+
MINS
diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/WTG1000_PFD.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/WTG1000_PFD.tsx index cc27593bc..47fd6d866 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/WTG1000_PFD.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/PFD/WTG1000_PFD.tsx @@ -4,22 +4,23 @@ /// import { - AdcPublisher, AutopilotInstrument, BaseInstrumentPublisher, Clock, CompositeLogicXMLHost, ControlPublisher, ElectricalPublisher, EventBus, FacilityLoader, - FacilityRepository, FlightPathAirplaneSpeedMode, FlightPathCalculator, FlightPlanner, FlightPlannerEvents, FSComponent, GNSSPublisher, HEventPublisher, InstrumentBackplane, - InstrumentEvents, LNavSimVarPublisher, MinimumsControlEvents, MinimumsSimVarPublisher, NavComInstrument, NavComSimVarPublisher, NavProcessor, PluginSystem, SimVarValueType, - SoundServer, - TrafficInstrument, UnitType, UserSettingSaveManager, VNavSimVarPublisher, Wait, XMLGaugeConfigFactory, XMLWarningFactory, - XPDRInstrument + AdcPublisher, AnnunciationType, AutopilotInstrument, AvionicsSystem, BaseInstrumentPublisher, CasEvents, Clock, CompositeLogicXMLHost, ControlPublisher, ControlSurfacesPublisher, + DebounceTimer, + ElectricalPublisher, EventBus, FacilityLoader, FacilityRepository, FlightPathAirplaneSpeedMode, FlightPathCalculator, FlightPlanner, FlightPlannerEvents, + FSComponent, GNSSPublisher, HEventPublisher, InstrumentBackplane, InstrumentEvents, LNavSimVarPublisher, MinimumsControlEvents, MinimumsSimVarPublisher, + NavComInstrument, NavComSimVarPublisher, NavProcessor, PluginSystem, SimVarValueType, SoundServer, Subject, TrafficInstrument, UnitType, + UserSettingSaveManager, VNavSimVarPublisher, Wait, XMLGaugeConfigFactory, XMLWarningFactory, XPDRInstrument } from '@microsoft/msfs-sdk'; -import { Fms, GarminAdsb, LNavDataSimVarPublisher, NavIndicatorController, TrafficAdvisorySystem } from '@microsoft/msfs-garminsdk'; +import { AdcSystem, Fms, GarminAdsb, LNavDataSimVarPublisher, NavIndicatorController, TrafficAdvisorySystem } from '@microsoft/msfs-garminsdk'; import { EIS } from '../MFD/Components/EIS'; import { MapInset } from '../PFD/Components/Overlays/MapInset'; import { BacklightManager } from '../Shared/Backlight/BacklightManager'; import { FlightPlanAsoboSync } from '../Shared/FlightPlanAsoboSync'; import { G1000ControlPublisher } from '../Shared/G1000Events'; -import { G1000AvionicsPlugin, G1000PluginBinder } from '../Shared/G1000Plugin'; +import { G1000PfdAvionicsPlugin } from '../Shared/G1000PfdPlugin'; +import { G1000PfdPluginBinder } from '../Shared/G1000Plugin'; import { AhrsPublisher } from '../Shared/Instruments/AhrsPublisher'; import { NavComRadio } from '../Shared/NavCom/NavComRadio'; import { G1000Config } from '../Shared/NavComConfig'; @@ -30,26 +31,29 @@ import { StartupLogo } from '../Shared/StartupLogo'; import { ADCAvionicsSystem, AHRSSystem, AvionicsComputerSystem, EngineAirframeSystem, G1000AvionicsSystem, MagnetometerSystem, SoundSystem, TransponderSystem } from '../Shared/Systems'; +import { ControlpadInputController, ControlpadTargetInstrument } from '../Shared/UI/Controllers/ControlpadInputController'; import { ContextMenuDialog } from '../Shared/UI/Dialogs/ContextMenuDialog'; import { MessageDialog } from '../Shared/UI/Dialogs/MessageDialog'; import { ALTUnitsMenu } from '../Shared/UI/Menus/ALTUnitsMenu'; import { MapHSILayoutMenu } from '../Shared/UI/Menus/MapHSILayoutMenu'; import { MapHSIMenu } from '../Shared/UI/Menus/MapHSIMenu'; -import { SoftKeyMenuSystem } from '../Shared/UI/Menus/SoftKeyMenuSystem'; import { EngineMenu } from '../Shared/UI/Menus/MFD/EngineMenu'; import { FuelRemMenu } from '../Shared/UI/Menus/MFD/FuelRemMenu'; import { LeanMenu } from '../Shared/UI/Menus/MFD/LeanMenu'; import { SystemMenu } from '../Shared/UI/Menus/MFD/SystemMenu'; import { PFDOptMenu } from '../Shared/UI/Menus/PFDOptMenu'; import { RootMenu } from '../Shared/UI/Menus/RootMenu'; +import { SoftKeyMenuSystem } from '../Shared/UI/Menus/SoftKeyMenuSystem'; import { SVTMenu } from '../Shared/UI/Menus/SVTMenu'; import { WindMenu } from '../Shared/UI/Menus/WindMenu'; import { XPDRCodeMenu } from '../Shared/UI/Menus/XPDRCodeMenu'; import { XPDRMenu } from '../Shared/UI/Menus/XPDRMenu'; import { SoftKeyBar } from '../Shared/UI/SoftKeyBar'; import { UnitsUserSettings } from '../Shared/Units/UnitsUserSettings'; +import { VSpeedUserSettingManager } from '../Shared/VSpeed/VSpeedUserSettings'; import { WaypointIconImageCache } from '../Shared/WaypointIconImageCache'; -import { AirspeedIndicator } from './Components/FlightInstruments/AirspeedIndicator'; +import { G1000AirspeedIndicator } from './Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicator'; +import { G1000AirspeedIndicatorDataProvider } from './Components/FlightInstruments/AirspeedIndicator/G1000AirspeedIndicatorDataProvider'; import { Altimeter } from './Components/FlightInstruments/Altimeter'; import { CAS } from './Components/FlightInstruments/CAS'; import { FlightDirector } from './Components/FlightInstruments/FlightDirector'; @@ -66,6 +70,7 @@ import { ADFDME } from './Components/UI/ADF-DME/ADFDME'; import { PFDSelectAirway } from './Components/UI/Airway/PFDSelectAirway'; import { Alerts } from './Components/UI/Alerts/Alerts'; import { AlertsSubject } from './Components/UI/Alerts/AlertsSubject'; +import { CasAlertsBridge } from './Components/UI/Alerts/CasAlertsBridge'; import { PFDDirectTo } from './Components/UI/DirectTo/PFDDirectTo'; import { FPL } from './Components/UI/FPL/FPL'; import { PFDHold } from './Components/UI/Hold/PFDHold'; @@ -85,14 +90,14 @@ import { VNavAlertForwarder } from './Components/VNavAlertForwarder'; import { WarningDisplay } from './Components/Warnings'; import '../Shared/UI/Common/g1k_common.css'; -import './WTG1000_PFD.css'; import '../Shared/UI/Common/LatLonDisplay.css'; +import './WTG1000_PFD.css'; /** * The base G1000 PFD instrument class. */ class WTG1000_PFD extends BaseInstrument { - private readonly bus: EventBus; + private readonly bus = new EventBus(); private readonly baseInstrumentPublisher: BaseInstrumentPublisher; private readonly adcPublisher: AdcPublisher; @@ -120,6 +125,7 @@ class WTG1000_PFD extends BaseInstrument { private readonly calculator: FlightPathCalculator; private readonly soundServer: SoundServer; private readonly minimumsPublisher: MinimumsSimVarPublisher; + private readonly controlSurfacesPublisher: ControlSurfacesPublisher; private lastCalculate = 0; @@ -139,14 +145,26 @@ class WTG1000_PFD extends BaseInstrument { private readonly airframeOptions: G1000AirframeOptionsManager; - private readonly systems: G1000AvionicsSystem[] = []; + private readonly g1000Systems: G1000AvionicsSystem[] = []; + private readonly systems: AvionicsSystem[] = []; + private readonly alerts: AlertsSubject; + private readonly casAlertsBridge: CasAlertsBridge; private isMfdPoweredOn = false; private gamePlanSynced = false; private vnavAlertForwarder: any; - private readonly pluginSystem = new PluginSystem(); + private readonly pluginSystem = new PluginSystem(); + + private airspeedIndicatorDataProvider!: G1000AirspeedIndicatorDataProvider; + + private vSpeedSettingManager!: VSpeedUserSettingManager; + + private readonly comRadio = FSComponent.createRef(); + private readonly navRadio = FSComponent.createRef(); + private controlPadHandler = new ControlpadInputController(this.bus, ControlpadTargetInstrument.PFD); + private casPowerDebounce = new DebounceTimer(); /** * Creates an instance of the WTG1000_PFD. @@ -158,8 +176,6 @@ class WTG1000_PFD extends BaseInstrument { WaypointIconImageCache.init(); - this.bus = new EventBus(); - this.baseInstrumentPublisher = new BaseInstrumentPublisher(this, this.bus); this.vnavAlertForwarder = new VNavAlertForwarder(this.bus); @@ -183,6 +199,8 @@ class WTG1000_PFD extends BaseInstrument { this.trafficInstrument = new TrafficInstrument(this.bus, { realTimeUpdateFreq: 2, simTimeUpdateFreq: 1, contactDeprecateTime: 10 }); this.minimumsPublisher = new MinimumsSimVarPublisher(this.bus); + this.controlSurfacesPublisher = new ControlSurfacesPublisher(this.bus, 3); + this.clock = new Clock(this.bus); this.facLoader = new FacilityLoader(FacilityRepository.getRepository(this.bus)); @@ -210,7 +228,7 @@ class WTG1000_PFD extends BaseInstrument { this.backplane.addPublisher('vnav', this.vNavPublisher); this.backplane.addPublisher('hEvents', this.hEventPublisher); this.backplane.addPublisher('control', this.controlPublisher); - this.backplane.addPublisher('g1000', this.g1000ControlPublisher); + this.backplane.addPublisher('g1000Control', this.g1000ControlPublisher); this.backplane.addPublisher('gnss', this.gnssPublisher); this.backplane.addPublisher('electrical', this.electricalPublisher); this.backplane.addPublisher('minimums', this.minimumsPublisher); @@ -222,6 +240,8 @@ class WTG1000_PFD extends BaseInstrument { this.backplane.addInstrument('xpdr', this.xpdrInstrument); this.backplane.addInstrument('traffic', this.trafficInstrument); + this.backplane.addPublisher('ControlSurfaces', this.controlSurfacesPublisher); + this.gaugeFactory = new XMLGaugeConfigFactory(this, this.bus); this.airframeOptions = new G1000AirframeOptionsManager(this, this.bus); @@ -246,6 +266,7 @@ class WTG1000_PFD extends BaseInstrument { this.settingSaveManager.load(saveKey); this.alerts = new AlertsSubject(this.bus); + this.casAlertsBridge = new CasAlertsBridge(this.bus); this.initDuration = 5000; } @@ -274,7 +295,9 @@ class WTG1000_PFD extends BaseInstrument { this.classList.add('hidden-element'); - this.backplane.init(); + this.airspeedIndicatorDataProvider = new G1000AirspeedIndicatorDataProvider(this.bus, this.airframeOptions.airspeedIndicatorConfig); + + this.vSpeedSettingManager = new VSpeedUserSettingManager(this.bus, this.airframeOptions.vSpeedGroups); const gaugeConfig = this.airframeOptions.gaugeConfig; const menuSystem = new SoftKeyMenuSystem(this.bus, 'AS1000_PFD_SOFTKEYS_'); @@ -282,7 +305,10 @@ class WTG1000_PFD extends BaseInstrument { this.pluginSystem.addScripts(this.xmlConfig, this.templateID, (target) => { return target === this.templateID; }).then(() => { - this.pluginSystem.startSystem({ bus: this.bus, viewService: this.viewService, menuSystem: menuSystem }).then(() => { + this.pluginSystem.startSystem({ + bus: this.bus, backplane: this.backplane, viewService: this.viewService, menuSystem: menuSystem, + fms: this.fms, navIndicatorController: this.navIndicatorController + }).then(() => { // if (alertsPopoutRef.instance !== null) { menuSystem.addMenu('root', new RootMenu(menuSystem, this.controlPublisher, this.g1000ControlPublisher, this.bus)); @@ -303,29 +329,46 @@ class WTG1000_PFD extends BaseInstrument { menuSystem.pushMenu('root'); - this.pluginSystem.callPlugins(p => p.onMenuSystemInitialized()); + this.pluginSystem.callPlugins(p => p.onMenuSystemInitialized?.()); FSComponent.render(, document.getElementById('HorizonContainer')); FSComponent.render(, document.getElementById('InstrumentsContainer')); FSComponent.render(, document.getElementById('InstrumentsContainer')); - FSComponent.render(, document.getElementById('InstrumentsContainer')); + FSComponent.render( + , + document.getElementById('InstrumentsContainer') + ); FSComponent.render(, document.getElementById('InstrumentsContainer')); FSComponent.render(, document.getElementById('InstrumentsContainer')); FSComponent.render(, document.getElementById('InstrumentsContainer')); FSComponent.render(, document.getElementById('InstrumentsContainer')); FSComponent.render(, document.getElementById('InstrumentsContainer')); - FSComponent.render(, document.querySelector('#NavComBox #Left')); - FSComponent.render(, document.querySelector('#NavComBox #Right')); + FSComponent.render(, document.querySelector('#NavComBox #Left')); + FSComponent.render(, document.querySelector('#NavComBox #Right')); FSComponent.render(, document.getElementById('NavComBox')); FSComponent.render(, document.getElementById('InstrumentsContainer')); FSComponent.render(, document.getElementById('Electricity')); FSComponent.render(, document.getElementById('InstrumentsContainer')); - FSComponent.render(, - document.getElementById('InstrumentsContainer')); + FSComponent.render(, document.getElementById('InstrumentsContainer')); FSComponent.render(, document.getElementById('cas')); FSComponent.render(, document.getElementById('warnings')); + + this.pluginSystem.callPlugins(plugin => { + + // At last, let the plugins directly render on the instruments container: + const node = plugin.renderToPfdInstruments?.(); + if (node) { + FSComponent.render(node, document.getElementById('InstrumentsContainer')); + } + }); + FSComponent.render(, this); FSComponent.render(, document.getElementsByClassName('eis')[0] as HTMLDivElement); @@ -340,7 +383,19 @@ class WTG1000_PFD extends BaseInstrument { this.viewService.registerView('SelectArrival', () => ); this.viewService.registerView(ContextMenuDialog.name, () => ); this.viewService.registerView('PageMenuDialog', () => ); - this.viewService.registerView(TimerRef.name, () => ); + this.viewService.registerView(TimerRef.name, () => { + return ( + + ); + }); this.viewService.registerView(ADFDME.name, () => ); this.viewService.registerView('WptDup', () => ); this.viewService.registerView(Nearest.name, () => ); @@ -349,11 +404,12 @@ class WTG1000_PFD extends BaseInstrument { this.viewService.registerView('SelectAirway', () => ); this.viewService.registerView('HoldAt', () => ); - this.pluginSystem.callPlugins(p => p.onViewServiceInitialized()); + this.pluginSystem.callPlugins(p => p.onViewServiceInitialized?.()); + this.backplane.init(); this.controlPublisher.publishEvent('init_cdi', true); this.bus.on('mfd_power_on', this.onMfdPowerOn); - + this.controlPadHandler.setFrequencyElementRefs(this.comRadio.instance, this.navRadio.instance); }); // force enable animations @@ -374,14 +430,18 @@ class WTG1000_PFD extends BaseInstrument { this.initializeAvElectrical(); - this.systems.push(new MagnetometerSystem(1, this.bus)); - this.systems.push(new ADCAvionicsSystem(1, this.bus)); - this.systems.push(new AHRSSystem(1, this.bus)); - this.systems.push(new TransponderSystem(1, this.bus)); - this.systems.push(new AvionicsComputerSystem(1, this.bus)); - this.systems.push(new AvionicsComputerSystem(2, this.bus)); - this.systems.push(new EngineAirframeSystem(1, this.bus)); - this.systems.push(new SoundSystem(1, this.bus, this.soundServer)); + this.g1000Systems.push(new MagnetometerSystem(1, this.bus)); + this.g1000Systems.push(new ADCAvionicsSystem(1, this.bus)); + this.g1000Systems.push(new AHRSSystem(1, this.bus)); + this.g1000Systems.push(new TransponderSystem(1, this.bus)); + this.g1000Systems.push(new AvionicsComputerSystem(1, this.bus)); + this.g1000Systems.push(new AvionicsComputerSystem(2, this.bus)); + this.g1000Systems.push(new EngineAirframeSystem(1, this.bus)); + this.g1000Systems.push(new SoundSystem(1, this.bus, this.soundServer)); + + this.systems.push(new AdcSystem(1, this.bus, 1, 1, 'elec_av1_bus')); + + this.airspeedIndicatorDataProvider.init(); const sub = this.bus.getSubscriber(); @@ -499,6 +559,10 @@ class WTG1000_PFD extends BaseInstrument { super.onPowerOn(); this.classList.remove('hidden-element'); + this.casPowerDebounce.clear(); + this.casPowerDebounce.schedule(() => { + this.bus.getPublisher().pub('cas_set_initial_acknowledge', false, true, true); + }, 2000); } /** @inheritdoc */ @@ -506,6 +570,12 @@ class WTG1000_PFD extends BaseInstrument { super.onShutDown(); this.classList.add('hidden-element'); + + this.bus.getPublisher().pub('cas_set_initial_acknowledge', true, true, true); + SimVar.SetSimVarValue('K:MASTER_CAUTION_ACKNOWLEDGE', SimVarValueType.Number, 0); + SimVar.SetSimVarValue('K:MASTER_WARNING_ACKNOWLEDGE', SimVarValueType.Number, 0); + this.bus.getPublisher().pub('cas_master_acknowledge', AnnunciationType.SafeOp, true, false); + this.bus.getPublisher().pub('cas_master_acknowledge', AnnunciationType.Advisory, true, false); } /** @@ -519,6 +589,8 @@ class WTG1000_PFD extends BaseInstrument { this.clock.onUpdate(); this.backplane.onUpdate(); + this.updateSystems(); + const now = Date.now(); if (now - this.lastCalculate > 3000) { if (this.planner.hasFlightPlan(this.planner.activePlanIndex)) { @@ -531,6 +603,15 @@ class WTG1000_PFD extends BaseInstrument { this.eisXmlLogicHost.update(this.deltaTime); } + /** + * Updates this instrument's systems. + */ + private updateSystems(): void { + for (let i = 0; i < this.systems.length; i++) { + this.systems[i].onUpdate(); + } + } + /** * Handles when the instrument screen state has changed. * @param newState The current screen state. @@ -603,8 +684,12 @@ class WTG1000_PFD extends BaseInstrument { * @param args The H event and associated arguments, if any. */ public onInteractionEvent(args: string[]): void { - this.hEventPublisher.dispatchHEvent(args[0]); + const isHandled = this.controlPadHandler.handleControlPadEventInput(args[0]); + if (isHandled === false) { + // If the controlpad handler did not handle the event, continue and publish: + this.hEventPublisher.dispatchHEvent(args[0]); + } } } -registerInstrument('wtg1000-pfd', WTG1000_PFD); \ No newline at end of file +registerInstrument('wtg1000-pfd', WTG1000_PFD); diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Autopilot/G1000Autopilot.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Autopilot/G1000Autopilot.ts index 208f086cd..c277ba7ae 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Autopilot/G1000Autopilot.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Autopilot/G1000Autopilot.ts @@ -1,36 +1,18 @@ -import { - AltitudeSelectManager, AltitudeSelectManagerOptions, APAltitudeModes, APConfig, APLateralModes, APStateManager, APVerticalModes, Autopilot, - EventBus, FlightPlanner, MetricAltitudeSettingsManager, ObjectSubject, Subject, UnitType -} from '@microsoft/msfs-sdk'; +import { APAltitudeModes, APLateralModes, APStateManager, APVerticalModes, EventBus, FlightPlanner, Subject } from '@microsoft/msfs-sdk'; + +import { FmaDataEvents, GarminAPConfigInterface, GarminAutopilot, GarminAutopilotOptions } from '@microsoft/msfs-garminsdk'; import { G1000ControlEvents } from '../G1000Events'; import { G1000APSimVarEvents } from '../Instruments/G1000APPublisher'; -import { FmaData } from './FmaData'; /** * A Garmin GFC700 autopilot. */ -export class G1000Autopilot extends Autopilot { - private static readonly ALT_SELECT_OPTIONS: AltitudeSelectManagerOptions = { - supportMetric: true, - minValue: UnitType.FOOT.createNumber(-1000), - maxValue: UnitType.FOOT.createNumber(50000), - inputIncrLargeThreshold: 999, - incrSmall: UnitType.FOOT.createNumber(100), - incrLarge: UnitType.FOOT.createNumber(1000), - incrSmallMetric: UnitType.METER.createNumber(50), - incrLargeMetric: UnitType.METER.createNumber(500), - initOnInput: true, - initToIndicatedAlt: true - }; - +export class G1000Autopilot extends GarminAutopilot { public readonly externalAutopilotInstalled = Subject.create(false); protected readonly lateralArmedModeSubject = Subject.create(APLateralModes.NONE); protected readonly altArmedSubject = Subject.create(false); - protected readonly altSelectManager = new AltitudeSelectManager(this.bus, this.settingsManager, G1000Autopilot.ALT_SELECT_OPTIONS); - - protected readonly fmaData: ObjectSubject; private fmaUpdateDebounce: NodeJS.Timeout | undefined; /** @@ -39,25 +21,18 @@ export class G1000Autopilot extends Autopilot { * @param flightPlanner This autopilot's associated flight planner. * @param config This autopilot's configuration. * @param stateManager This autopilot's state manager. - * @param settingsManager The settings manager to pass to altitude preselect system. + * @param options Options with which to configure the new autopilot. */ - constructor(bus: EventBus, flightPlanner: FlightPlanner, config: APConfig, stateManager: APStateManager, - private readonly settingsManager: MetricAltitudeSettingsManager) { - super(bus, flightPlanner, config, stateManager); - - this.fmaData = ObjectSubject.create({ - verticalActive: APVerticalModes.NONE, - verticalArmed: APVerticalModes.NONE, - verticalApproachArmed: APVerticalModes.NONE, - verticalAltitudeArmed: APAltitudeModes.NONE, - altitideCaptureArmed: false, - altitideCaptureValue: -1, - lateralActive: APLateralModes.NONE, - lateralArmed: APLateralModes.NONE, - lateralModeFailed: false - }); - - const publisher = this.bus.getPublisher(); + constructor( + bus: EventBus, + flightPlanner: FlightPlanner, + config: GarminAPConfigInterface, + stateManager: APStateManager, + options?: Readonly + ) { + super(bus, flightPlanner, config, stateManager, options); + + const publisher = this.bus.getPublisher(); this.fmaData.sub(() => { // dirty debounce, need better ObjectSubject if (this.fmaUpdateDebounce) { @@ -67,6 +42,7 @@ export class G1000Autopilot extends Autopilot { this.fmaUpdateDebounce = setTimeout(() => { this.fmaUpdateDebounce = undefined; publisher.pub('fma_modes', this.fmaData.get(), true); + publisher.pub('fma_data', Object.assign({}, this.fmaData.get()), true, true); //For GoAroundManager }, 0); }, true); } @@ -74,22 +50,17 @@ export class G1000Autopilot extends Autopilot { /** @inheritdoc */ protected onAfterUpdate(): void { if (!this.externalAutopilotInstalled.get()) { - this.updateFma(); + this.updateG1000Fma(); } else { this.lateralArmedModeSubject.set(this.apValues.lateralArmed.get()); this.altArmedSubject.set(this.altCapArmed); } } - /** @inheritdoc */ - protected onInitialized(): void { - this.bus.pub('vnav_set_state', true); - - this.monitorAdditionalEvents(); - } - /** @inheritdoc */ protected monitorAdditionalEvents(): void { + super.monitorAdditionalEvents(); + //check for KAP140 installed const g1000APSimvars = this.bus.getSubscriber(); g1000APSimvars.on('kap_140_installed').whenChanged().handle(this.setExternalAutopilotInstalled.bind(this)); @@ -108,7 +79,7 @@ export class G1000Autopilot extends Autopilot { this.config.defaultLateralMode = APLateralModes.LEVEL; this.altSelectManager.setEnabled(false); this.handleApFdStateChange(); - this.updateFma(true); + this.updateG1000Fma(true); this.bus.getPublisher().pub('fd_not_installed', true, true); } @@ -118,7 +89,7 @@ export class G1000Autopilot extends Autopilot { * Publishes data for the FMA. * @param clear Is to clear the FMA */ - private updateFma(clear = false): void { + private updateG1000Fma(clear = false): void { const fmaTemp = this.fmaData; fmaTemp.set('verticalApproachArmed', (clear ? APVerticalModes.NONE : this.verticalApproachArmed)); fmaTemp.set('verticalArmed', (clear ? APVerticalModes.NONE : this.apValues.verticalArmed.get())); diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/Config.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/Config.ts new file mode 100644 index 000000000..bbf421e9e --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/Config.ts @@ -0,0 +1,34 @@ +/** + * A configuration object. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface Config { + +} + +/** + * A configuration object which can be resolved to a value. + */ +export interface ResolvableConfig extends Config { + /** Flags this object as a ResolvableConfig. */ + readonly isResolvableConfig: true; + + /** + * Resolves this config to a value. + * @returns This config's resolved value. + */ + resolve(): T; +} + +/** + * A configuration object factory. + */ +export interface ConfigFactory { + /** + * Creates a configuration object from a configuration document element. + * @param element A configuration document element. + * @returns The configuration object defined by the specified element, or `undefined` if the element does not define + * a configuration object recognized by this factory. + */ + create(element: Element): Config | undefined; +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/DefaultConfigFactory.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/DefaultConfigFactory.ts new file mode 100644 index 000000000..a9d1f12ad --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/DefaultConfigFactory.ts @@ -0,0 +1,28 @@ +import { SpeedConfig } from './SpeedConfig'; +import { Config, ConfigFactory } from './Config'; +import { LookupTableConfig } from './LookupTableConfig'; +import { NumericConstantConfig, NumericMaxConfig, NumericMinConfig } from './NumericConfig'; + +/** + * A default implementation of {@link ConfigFactory}. + */ +export class DefaultConfigFactory implements ConfigFactory { + private static readonly TAG_MAP: Record Config> = { + 'Number': NumericConstantConfig, + 'Min': NumericMinConfig, + 'Max': NumericMaxConfig, + 'LookupTable': LookupTableConfig, + 'Speed': SpeedConfig + }; + + /** @inheritdoc */ + public create(element: Element): Config | undefined { + const ctor = DefaultConfigFactory.TAG_MAP[element.tagName]; + + if (ctor === undefined) { + return undefined; + } + + return new ctor(element, this); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/LookupTableConfig.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/LookupTableConfig.ts new file mode 100644 index 000000000..d22ee29c9 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/LookupTableConfig.ts @@ -0,0 +1,73 @@ +import { LerpLookupTable } from '@microsoft/msfs-sdk'; + +import { ResolvableConfig } from './Config'; + +/** + * A configuration object which defines a lookup table. + */ +export class LookupTableConfig implements ResolvableConfig { + /** @inheritdoc */ + public readonly isResolvableConfig = true; + + /** The dimension count of this config's lookup table. */ + public readonly dimensions: number; + + /** The breakpoints of this config's lookup table. */ + public readonly breakpoints: readonly (readonly number[])[]; + + /** + * Creates a new LookupTableConfig from a configuration document element. + * @param element A configuration document element. + */ + constructor(element: Element) { + if (element.tagName !== 'LookupTable') { + throw new Error(`Invalid LookupTableConfig definition: expected tag name 'LookupTable' but was '${element.tagName}'`); + } + + const dimensions = element.getAttribute('dimensions'); + if (dimensions === null) { + throw new Error('Invalid LookupTableConfig definition: undefined \'dimensions\' attribute'); + } + + const parsedDimensions = Number(dimensions); + if (isNaN(parsedDimensions) || Math.trunc(parsedDimensions) !== parsedDimensions || parsedDimensions <= 0) { + throw new Error(`Invalid LookupTableConfig definition: expected 'dimensions' to be a positive integer but was '${dimensions}'`); + } + this.dimensions = parsedDimensions; + + const value = element.textContent; + if (value === null) { + throw new Error('Invalid LookupTableConfig definition: undefined value'); + } + + let parsedValue: any = undefined; + try { + parsedValue = JSON.parse(value); + } catch { + // continue + } + + if (parsedValue instanceof Array) { + for (const breakpoint of parsedValue) { + if (!(breakpoint instanceof Array && breakpoint.length === parsedDimensions + 1 && breakpoint.every(el => typeof el === 'number'))) { + throw new Error('Invalid LookupTableConfig definition: malformed lookup table array'); + } + } + } else { + throw new Error('Invalid LookupTableConfig definition: value was not an array'); + } + + this.breakpoints = parsedValue; + } + + /** @inheritdoc */ + public resolve(): LerpLookupTable { + const table = new LerpLookupTable(this.dimensions); + + for (const breakpoint of this.breakpoints) { + table.insertBreakpoint(breakpoint); + } + + return table; + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/NumericConfig.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/NumericConfig.ts new file mode 100644 index 000000000..9e0e335ce --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/NumericConfig.ts @@ -0,0 +1,321 @@ +import { MappedSubject, MappedSubscribable, MutableSubscribable, Subscription } from '@microsoft/msfs-sdk'; + +import { ConfigFactory, ResolvableConfig } from './Config'; + +/** + * A configuration object which defines a factory for a numeric value. + */ +export interface NumericConfig extends ResolvableConfig<(context?: any) => number | MappedSubscribable> { + /** Flags this object as a NumericConfig. */ + readonly isNumericConfig: true; +} + +/** + * A configuration object which defines a factory for a numeric constant. + */ +export class NumericConstantConfig implements NumericConfig { + public readonly isResolvableConfig = true; + public readonly isNumericConfig = true; + + /** The numeric value of this config. */ + public readonly value: number; + + /** + * Creates a new NumericConstantConfig from a configuration document element. + * @param element A configuration document element. + */ + constructor(element: Element) { + if (element.tagName !== 'Number') { + throw new Error(`Invalid NumericConstantConfig definition: expected tag name 'Number' but was '${element.tagName}'`); + } + + const value = element.textContent; + if (value === null) { + throw new Error('Invalid NumericConstantConfig definition: undefined value'); + } + + const parsedValue = Number(value); + if (isNaN(parsedValue)) { + throw new Error('Invalid NumericConstantConfig definition: value was not a number'); + } + + this.value = parsedValue; + } + + /** @inheritdoc */ + public resolve(): () => number { + return () => this.value; + } +} + +/** + * A configuration object which defines a factory for a numeric value which is the minimum of one or more inputs. + */ +export class NumericMinConfig implements NumericConfig { + public readonly isResolvableConfig = true; + public readonly isNumericConfig = true; + + /** The inputs of this config. */ + public readonly inputs: readonly NumericConfig[]; + + /** + * Creates a new NumericMinConfig from a configuration document element. + * @param element A configuration document element. + * @param factory A configuration object factory to use to create child configuration objects. + */ + constructor(element: Element, factory: ConfigFactory) { + if (element.tagName !== 'Min') { + throw new Error(`Invalid NumericMinConfig definition: expected tag name 'Min' but was '${element.tagName}'`); + } + + const args = []; + + for (const child of element.children) { + const config = factory.create(child); + if (config !== undefined && 'isNumericConfig' in config) { + args.push(config as NumericConfig); + } + } + + if (args.length === 0) { + throw new Error('Invalid NumericMinConfig definition: found zero inputs (must have at least one)'); + } + + this.inputs = args; + } + + /** @inheritdoc */ + public resolve(): (context?: any) => number | MappedSubscribable { + return (context?: any): number | MappedSubscribable => { + const resolvedArgs = this.inputs.map(arg => typeof arg === 'number' ? arg : arg.resolve()(context)); + + if (resolvedArgs.some(arg => typeof arg === 'object')) { + if (resolvedArgs.length === 1) { + return resolvedArgs[0]; + } else { + const numbers = resolvedArgs.filter(arg => typeof arg === 'number') as number[]; + const subscribables = resolvedArgs.filter(arg => typeof arg === 'object') as MappedSubscribable[]; + const min = Math.min(...numbers, Number.POSITIVE_INFINITY); + + return new ChainedMappedSubscribable( + MappedSubject.create( + (args: readonly number[]): number => { + return Math.min(min, ...args); + }, + ...subscribables + ), + subscribables + ); + } + } else { + return Math.min(...resolvedArgs as number[]); + } + }; + } +} + +/** + * A configuration object which defines a factory for a numeric value which is the maximum of one or more inputs. + */ +export class NumericMaxConfig implements NumericConfig { + public readonly isResolvableConfig = true; + public readonly isNumericConfig = true; + + /** The inputs of this config. */ + public readonly inputs: readonly NumericConfig[]; + + /** + * Creates a new NumericMaxConfig from a configuration document element. + * @param element A configuration document element. + * @param factory A configuration object factory to use to create child configuration objects. + */ + constructor(element: Element, factory: ConfigFactory) { + if (element.tagName !== 'Max') { + throw new Error(`Invalid NumericMaxConfig definition: expected tag name 'Max' but was '${element.tagName}'`); + } + + const args = []; + + for (const child of element.children) { + const config = factory.create(child); + if (config !== undefined && 'isNumericConfig' in config) { + args.push(config as NumericConfig); + } + } + + if (args.length === 0) { + throw new Error('Invalid NumericMaxConfig definition: found zero inputs (must have at least one)'); + } + + this.inputs = args; + } + + /** @inheritdoc */ + public resolve(): (context?: any) => number | MappedSubscribable { + return (context?: any): number | MappedSubscribable => { + const resolvedArgs = this.inputs.map(arg => typeof arg === 'number' ? arg : arg.resolve()(context)); + + if (resolvedArgs.some(arg => typeof arg === 'object')) { + if (resolvedArgs.length === 1) { + return resolvedArgs[0]; + } else { + const numbers = resolvedArgs.filter(arg => typeof arg === 'number') as number[]; + const subscribables = resolvedArgs.filter(arg => typeof arg === 'object') as MappedSubscribable[]; + const max = Math.max(...numbers, Number.NEGATIVE_INFINITY); + + return new ChainedMappedSubscribable( + MappedSubject.create( + (args: readonly number[]): number => { + return Math.max(max, ...args); + }, + ...subscribables + ), + subscribables + ); + } + } else { + return Math.max(...resolvedArgs as number[]); + } + }; + } +} + +/** + * A subscribable which wraps a mapped subscribable chained from another mapped subscribable. Pause/resume/destroy + * operations on this subscribable are transferred to the source of the wrapped subscribable. + */ +class ChainedMappedSubscribable implements MappedSubscribable { + /** @inheritdoc */ + public readonly isSubscribable = true; + + /** @inheritdoc */ + public readonly canInitialNotify = true; + + /** @inheritdoc */ + public get isAlive(): boolean { + return this.mapped.isAlive; + } + + /** @inheritdoc */ + public get isPaused(): boolean { + return this.mapped.isPaused; + } + + /** + * Constructor. + * @param mapped The mapped subscribable wrapped by this chained subscribable. + * @param sources The sources of this chained subscribable's wrapped subscribable. + */ + constructor( + private readonly mapped: MappedSubscribable, + private readonly sources: MappedSubscribable[] + ) { + } + + /** @inheritdoc */ + public get(): number { + return this.mapped.get(); + } + + /** @inheritdoc */ + public sub(handler: (value: number) => void, initialNotify?: boolean, paused?: boolean): Subscription { + return this.mapped.sub(handler, initialNotify, paused); + } + + /** @inheritdoc */ + public unsub(handler: (value: number) => void): void { + this.mapped.unsub(handler); + } + + /** + * Maps this subscribable to a new subscribable. + * @param fn The function to use to map to the new subscribable. + * @param equalityFunc The function to use to check for equality between mapped values. Defaults to the strict + * equality comparison (`===`). + * @returns The mapped subscribable. + */ + public map(fn: (input: number, previousVal?: M) => M, equalityFunc?: ((a: M, b: M) => boolean)): MappedSubscribable; + /** + * Maps this subscribable to a new subscribable with a persistent, cached value which is mutated when it changes. + * @param fn The function to use to map to the new subscribable. + * @param equalityFunc The function to use to check for equality between mapped values. + * @param mutateFunc The function to use to change the value of the mapped subscribable. + * @param initialVal The initial value of the mapped subscribable. + * @returns The mapped subscribable. + */ + public map( + fn: (input: number, previousVal?: M) => M, + equalityFunc: (a: M, b: M) => boolean, + mutateFunc: (oldVal: M, newVal: M) => void, + initialVal: M + ): MappedSubscribable; + // eslint-disable-next-line jsdoc/require-jsdoc + public map( + fn: (input: number, previousVal?: M) => M, + equalityFunc?: (a: M, b: M) => boolean, + mutateFunc?: (oldVal: M, newVal: M) => void, + initialVal?: M + ): MappedSubscribable { + if (mutateFunc === undefined) { + return this.mapped.map(fn, equalityFunc); + } else { + return this.mapped.map(fn, equalityFunc as (a: M, b: M) => boolean, mutateFunc, initialVal as M); + } + } + + /** + * Subscribes to and pipes this subscribable's state to a mutable subscribable. Whenever an update of this + * subscribable's state is received through the subscription, it will be used as an input to change the other + * subscribable's state. + * @param to The mutable subscribable to which to pipe this subscribable's state. + * @param paused Whether the new subscription should be initialized as paused. Defaults to `false`. + * @returns The new subscription. + */ + public pipe(to: MutableSubscribable, paused?: boolean): Subscription; + /** + * Subscribes to this subscribable's state and pipes a mapped version to a mutable subscribable. Whenever an update + * of this subscribable's state is received through the subscription, it will be transformed by the specified mapping + * function, and the transformed state will be used as an input to change the other subscribable's state. + * @param to The mutable subscribable to which to pipe this subscribable's mapped state. + * @param map The function to use to transform inputs. + * @param paused Whether the new subscription should be initialized as paused. Defaults to `false`. + * @returns The new subscription. + */ + public pipe(to: MutableSubscribable, map: (fromVal: number, toVal: M) => M, paused?: boolean): Subscription; + // eslint-disable-next-line jsdoc/require-jsdoc + public pipe(to: MutableSubscribable | MutableSubscribable, arg2?: ((fromVal: number, toVal: M) => M) | boolean, arg3?: boolean): Subscription { + if (typeof arg2 === 'function') { + return this.mapped.pipe(to as MutableSubscribable, arg2, arg3); + } else { + return this.mapped.pipe(to as MutableSubscribable, arg2 as boolean | undefined); + } + } + + /** @inheritdoc */ + public pause(): this { + this.mapped.pause(); + for (let i = 0; i < this.sources.length; i++) { + this.sources[i].pause(); + } + + return this; + } + + /** @inheritdoc */ + public resume(): this { + for (let i = 0; i < this.sources.length; i++) { + this.sources[i].resume(); + } + this.mapped.resume(); + + return this; + } + + /** @inheritdoc */ + public destroy(): void { + this.mapped.destroy(); + for (let i = 0; i < this.sources.length; i++) { + this.sources[i].destroy(); + } + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/SpeedConfig.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/SpeedConfig.ts new file mode 100644 index 000000000..77337c028 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/SpeedConfig.ts @@ -0,0 +1,209 @@ +import { MappedSubject, MappedSubscribable, SubscribableUtils } from '@microsoft/msfs-sdk'; + +import { AirspeedDefinitionContext } from '@microsoft/msfs-garminsdk'; + +import { LookupTableConfig } from './LookupTableConfig'; +import { NumericConfig } from './NumericConfig'; +import { VSpeedValueKey } from '../VSpeed/VSpeed'; + +/** + * Types of speed configs. + */ +export enum SpeedConfigType { + Ias = 'Ias', + Mach = 'Mach', + Tas = 'Tas', + Aoa = 'Aoa', + Reference = 'Reference' +} + +/** + * A configuration object which defines a factory for an airspeed value presented as knots indicated airspeed. + * + * The airspeed value can be defined from a specific indicated airspeed, mach number, true airspeed, or angle-of-attack + * value, from a one-dimensional lookup table of any of the previous value types keyed on pressure altitude, or from an + * aircraft reference speed. + */ +export class SpeedConfig implements NumericConfig { + public readonly isResolvableConfig = true; + public readonly isNumericConfig = true; + + /** The type of this config. */ + public readonly type: SpeedConfigType; + + /** The value of this config. */ + public readonly value: number | LookupTableConfig | VSpeedValueKey; + + /** + * Creates a new SpeedConfig from a configuration document element. + * @param element A configuration document element. + */ + constructor(element: Element) { + if (element.tagName !== 'Speed') { + throw new Error(`Invalid SpeedConfig definition: expected tag name 'Speed' but was '${element.tagName}'`); + } + + const type = element.getAttribute('type'); + switch (type) { + case SpeedConfigType.Ias: + case SpeedConfigType.Mach: + case SpeedConfigType.Tas: + case SpeedConfigType.Aoa: + case SpeedConfigType.Reference: + this.type = type; + break; + default: + throw new Error(`Invalid SpeedConfig definition: unrecognized type '${type}'`); + } + + if (this.type === SpeedConfigType.Reference) { + const value = element.textContent; + if (value === null) { + throw new Error('Invalid SpeedConfig definition: undefined value'); + } + + if (Object.values(VSpeedValueKey).includes(value as any)) { + this.value = value as VSpeedValueKey; + } else { + throw new Error(`Invalid SpeedConfig definition: unrecognized value ${value} (value must be a valid reference speed key)`); + } + } else { + const lookupTable = element.querySelector(':scope>LookupTable'); + + if (lookupTable !== null) { + this.value = new LookupTableConfig(lookupTable); + } else { + const value = element.textContent; + if (value === null) { + throw new Error('Invalid SpeedConfig definition: undefined value'); + } + + const parsedValue = Number(value); + if (isNaN(parsedValue)) { + throw new Error('Invalid SpeedConfig definition: value was not a number or a lookup table'); + } + + this.value = parsedValue; + } + } + } + + /** @inheritdoc */ + public resolve(): (context: AirspeedDefinitionContext) => number | MappedSubscribable { + switch (this.type) { + case SpeedConfigType.Ias: + return this.resolveIas(); + case SpeedConfigType.Mach: + return this.resolveMach(); + case SpeedConfigType.Tas: + return this.resolveTas(); + case SpeedConfigType.Aoa: + return this.resolveAoa(); + case SpeedConfigType.Reference: + return this.resolveReference(); + } + } + + /** + * Resolves this config as a factory for an airspeed value defined from indicated airspeed. + * @returns A factory for an airspeed value defined from indicated airspeed. + */ + private resolveIas(): (context: AirspeedDefinitionContext) => number | MappedSubscribable { + const value = this.value as number | LookupTableConfig; + + return (context: AirspeedDefinitionContext) => { + if (typeof value === 'number') { + return value; + } else { + const table = value.resolve(); + + return context.pressureAlt.map(indicatedAlt => table.get(indicatedAlt)); + } + }; + } + + /** + * Resolves this config as a factory for an airspeed value defined from mach number. + * @returns A factory for an airspeed value defined from mach number. + */ + private resolveMach(): (context: AirspeedDefinitionContext) => number | MappedSubscribable { + const value = this.value as number | LookupTableConfig; + + return (context: AirspeedDefinitionContext) => { + if (typeof value === 'number') { + return context.machToKias.map(machToKias => machToKias * value); + } else { + const table = value.resolve(); + + return MappedSubject.create( + ([indicatedAlt, machToKias]): number => { + return table.get(indicatedAlt) * machToKias; + }, + context.pressureAlt, + context.machToKias + ); + } + }; + } + + /** + * Resolves this config as a factory for an airspeed value defined from true airspeed. + * @returns A factory for an airspeed value defined from true airspeed. + */ + private resolveTas(): (context: AirspeedDefinitionContext) => number | MappedSubscribable { + const value = this.value as number | LookupTableConfig; + + return (context: AirspeedDefinitionContext) => { + if (typeof value === 'number') { + return context.tasToIas.map(tasToIas => tasToIas * value); + } else { + const table = value.resolve(); + + return MappedSubject.create( + ([indicatedAlt, tasToIas]): number => { + return table.get(indicatedAlt) * tasToIas; + }, + context.pressureAlt, + context.tasToIas + ); + } + }; + } + + /** + * Resolves this config as a factory for an airspeed value defined from angle of attack. + * @returns A factory for an airspeed value defined from angle of attack. + */ + private resolveAoa(): (context: AirspeedDefinitionContext) => number | MappedSubscribable { + const value = this.value as number | LookupTableConfig; + + return (context: AirspeedDefinitionContext) => { + if (typeof value === 'number') { + return context.normAoaIasCoef.map((coef) => coef === null ? NaN : context.estimateIasFromNormAoa(value), SubscribableUtils.NUMERIC_NAN_EQUALITY); + } else { + const table = value.resolve(); + + return MappedSubject.create( + ([indicatedAlt, coef]): number => { + return coef === null ? NaN : context.estimateIasFromNormAoa(table.get(indicatedAlt)); + }, + SubscribableUtils.NUMERIC_NAN_EQUALITY, + context.pressureAlt, + context.normAoaIasCoef + ); + } + }; + } + + /** + * Resolves this config as a factory for an airspeed value defined from a reference speed. + * @returns A factory for an airspeed value defined from a reference speed. + */ + private resolveReference(): (context: AirspeedDefinitionContext) => number | MappedSubscribable { + const value = this.value as VSpeedValueKey; + + return () => { + return Math.round(Simplane.getDesignSpeeds()[value]); + }; + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/index.ts new file mode 100644 index 000000000..f2fb4d14b --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Config/index.ts @@ -0,0 +1,5 @@ +export * from './Config'; +export * from './DefaultConfigFactory'; +export * from './LookupTableConfig'; +export * from './NumericConfig'; +export * from './SpeedConfig'; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/FuelComputer.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/FuelComputer.ts index 55ce80245..064b9746c 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/FuelComputer.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/FuelComputer.ts @@ -21,7 +21,7 @@ export type FuelRemaingAdjustment = { } /** Simvars for the fuel computer to monitor. */ -interface FuelSimVars { +export interface FuelSimVars { /** The amount of fuel remaining. */ fuelQty: number; /** The flow on engine 1. */ @@ -30,6 +30,18 @@ interface FuelSimVars { fuelFlow2: number; } +/** Simvars for the fuel totalizer to publish. */ +export interface FuelTotalizerSimVars { + /** The amount of fuel remaining in the fuel totalizer. */ + remainingFuel: number; + /** The amount of fuel burned in the fuel totalizer. */ + burnedFuel: number; + /** The fuel endurance calculated by the fuel totalizer. */ + fuelEndurance: number; + /** The fuel range calculated by the fuel totalizer. */ + fuelRange: number; +} + /** A publisher to poll fuel-related simvars. */ class FuelSimVarPublisher extends SimVarPublisher { private static simvars = new Map([ @@ -47,6 +59,24 @@ class FuelSimVarPublisher extends SimVarPublisher { } } +/** A publisher to poll fuel-related simvars. */ +class FuelTotalizerSimVarPublisher extends SimVarPublisher { + private static simvars = new Map([ + ['remainingFuel', { name: FuelComputerSimVars.Remaining, type: SimVarValueType.GAL }], + ['burnedFuel', { name: FuelComputerSimVars.Burned, type: SimVarValueType.GAL }], + ['fuelEndurance', { name: FuelComputerSimVars.Endurance, type: SimVarValueType.GPH }], + ['fuelRange', { name: FuelComputerSimVars.Range, type: SimVarValueType.NM }], + ]); + + /** + * Create a FuelSimVarPublisher + * @param bus The EventBus to publish to + */ + public constructor(bus: EventBus) { + super(FuelTotalizerSimVarPublisher.simvars, bus); + } +} + /** A simple fuel totalizer and related logic. */ class Totalizer { private _fuelCapacity = 0; @@ -165,13 +195,17 @@ export class FuelComputer { private simVarSubscriber: EventSubscriber; private controlSubscriber: EventSubscriber; private totalizer: Totalizer; + private totalizerSimVarPublisher: FuelTotalizerSimVarPublisher; + + private readonly controlPublisher = this.bus.getPublisher(); /** * Create a fuel computer. * @param bus An event bus */ - constructor(bus: EventBus) { + constructor(private readonly bus: EventBus) { this.simVarPublisher = new FuelSimVarPublisher(bus); + this.totalizerSimVarPublisher = new FuelTotalizerSimVarPublisher(bus); this.simVarSubscriber = bus.getSubscriber(); this.controlSubscriber = bus.getSubscriber(); this.totalizer = new Totalizer(); @@ -180,10 +214,14 @@ export class FuelComputer { /** Intialize the instrument. */ public init(): void { this.simVarPublisher.startPublish(); + this.totalizerSimVarPublisher.startPublish(); this.totalizer.setCapacity(SimVar.GetSimVarValue('FUEL TOTAL CAPACITY', 'gallons') - SimVar.GetSimVarValue('UNUSABLE FUEL TOTAL QUANTITY', 'gallons')); this.totalizer.fuelRemaining = SimVar.GetSimVarValue('FUEL TOTAL QUANTITY', 'gallons'); this.simVarSubscriber.on('fuelQty').whenChangedBy(0.1).handle((qty) => { this.totalizer.rawQty = qty; }); - this.controlSubscriber.on('fuel_adjustment').handle((adjustment) => { this.totalizer.adjust(adjustment); }); + this.controlSubscriber.on('fuel_adjustment').handle((adjustment) => { + this.totalizer.adjust(adjustment); + this.controlPublisher.pub('fuel_adjusted_qty', this.totalizer.fuelRemaining); + }); this.controlSubscriber.on('fuel_comp_reset').handle((flag) => { this.totalizer.reset(flag); }); } @@ -192,5 +230,6 @@ export class FuelComputer { */ public onUpdate(): void { this.simVarPublisher.onUpdate(); + this.totalizerSimVarPublisher.onUpdate(); } } \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000Events.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000Events.ts index ed96520ea..bde4dc590 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000Events.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000Events.ts @@ -5,11 +5,9 @@ import { ProcedureType } from '@microsoft/msfs-garminsdk'; import { EISPageTypes } from '../MFD/Components/EIS'; import { NearestAirportSoftKey } from '../MFD/Components/UI/Nearest/MFDNearestAirportsPage'; import { NearestVorSoftKey } from '../MFD/Components/UI/Nearest/MFDNearestVORsPage'; -import { VSpeed } from '../PFD/Components/FlightInstruments/AirspeedIndicator'; import { FmaData } from './Autopilot/FmaData'; import { FuelRemaingAdjustment } from './FuelComputer'; - /** Extension of generic ControlEvents to handle G1000-specific events. */ export interface G1000ControlEvents { /** Event representing pfd alert button push. */ @@ -33,15 +31,12 @@ export interface G1000ControlEvents { /** Event representing xpdr code menu button push. */ xpdr_code_digit: number - /** Event for updating the v speeds from the soft menu to the airspeed indicator. */ - vspeed_set: VSpeed - - /** Event for updating the display of v speeds from the soft menu to the airspeed indicator. */ - vspeed_display: VSpeed - /** Event for updating the timer display. */ timer_value: number; + /** Sending reversionary EIS tab selections. */ + eis_reversionary_tab_select: EISPageTypes; + /** Sending EIS page selections. */ eis_page_select: EISPageTypes; @@ -54,6 +49,9 @@ export interface G1000ControlEvents { /** Adjust the remaining fuel total in the fuel computer. */ fuel_adjustment: FuelRemaingAdjustment; + /** Fuel in the totalizer after adjustment, gallons. */ + fuel_adjusted_qty: number; + /** Reset the fuel burn total. */ fuel_comp_reset: boolean; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000PfdPlugin.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000PfdPlugin.ts new file mode 100644 index 000000000..b4e73c225 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000PfdPlugin.ts @@ -0,0 +1,16 @@ +import { VNode } from '@microsoft/msfs-sdk'; + +import { G1000AvionicsPlugin, G1000PfdPluginBinder } from './G1000Plugin'; + +/** + * A G1000 PFD plugin. + */ +export abstract class G1000PfdAvionicsPlugin extends G1000AvionicsPlugin { + + /** Renders components to the PFD instrument container. + * @returns The rendered vnode + */ + public renderToPfdInstruments?(): VNode | null { + return null; + } +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000Plugin.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000Plugin.ts index 35c838b90..808519c92 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000Plugin.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/G1000Plugin.ts @@ -1,12 +1,11 @@ -import { AvionicsPlugin, EventBus } from '@microsoft/msfs-sdk'; +import { AvionicsPlugin, EventBus, InstrumentBackplane, VNode } from '@microsoft/msfs-sdk'; import { SoftKeyMenuSystem } from './UI/Menus/SoftKeyMenuSystem'; import { ViewService } from './UI/ViewService'; import { PageSelectMenuSystem } from '../MFD'; +import { Fms, NavIndicatorController } from '@microsoft/msfs-garminsdk'; -/** - * A plugin binder for G1000 plugins. - */ +/** A plugin binder for G1000 plugins. */ export interface G1000PluginBinder { /** The softkey menu system. */ menuSystem: SoftKeyMenuSystem; @@ -17,27 +16,61 @@ export interface G1000PluginBinder { /** The system-wide event bus. */ bus: EventBus; + /** The backplane instance. */ + backplane: InstrumentBackplane; + + /** The flight management system. */ + fms: Fms; + /** The FMS knob menu system (only needed on the MFD, as it does not move to the PFD in reversionary mode). */ pageSelectMenuSystem?: PageSelectMenuSystem; } +/** A plugin binder for G1000 PFD plugin. */ +export interface G1000PfdPluginBinder extends G1000PluginBinder { + /** The flight management system. */ + fms: Fms; + + /** An instance of the nav indicator controller. */ + navIndicatorController: NavIndicatorController; +} + +/** A plugin binder for G1000 MFD plugin. */ +export interface G1000MfdPluginBinder extends G1000PluginBinder { + /** The flight management system. */ + fms: Fms; +} + /** * An avionics plugin for the G1000 NXi. */ -export abstract class G1000AvionicsPlugin extends AvionicsPlugin { +export abstract class G1000AvionicsPlugin extends AvionicsPlugin { /** * A lifecycle callback called when the G1000 softkey menu system has been initialized. */ - public abstract onMenuSystemInitialized(): void; + public onMenuSystemInitialized?(): void { + return; + } /** * A lifecycle callback called when the G1000 page view service has been initialized. */ - public abstract onViewServiceInitialized(): void; + public onViewServiceInitialized?(): void { + return; + } /** * A lifecycle callback called when the G1000 rotary menu system has been initialized. */ - public abstract onPageSelectMenuSystemInitialized(): void; -} \ No newline at end of file + public onPageSelectMenuSystemInitialized?(): void { + return; + } + + + /** @returns null. Callback for rendering the EIS from a plugin. + */ + public renderEIS?(): VNode | null { + return null; + } +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Input/AltimeterBaroKeyEventHandler.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Input/AltimeterBaroKeyEventHandler.ts index d4e9c6419..c36e7383b 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Input/AltimeterBaroKeyEventHandler.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Input/AltimeterBaroKeyEventHandler.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { EventBus, KeyEventData, KeyEventManager, KeyEvents, Subscription } from '@microsoft/msfs-sdk'; +import { ControlEvents, EventBus, KeyEventData, KeyEventManager, KeyEvents, Subscription } from '@microsoft/msfs-sdk'; + +import { G1000ControlEvents } from '../G1000Events'; /** * A handler for altimeter barometric setting key events. @@ -63,6 +65,7 @@ export class AltimeterBaroKeyEventHandler { this.isInit = true; this.keyEventManager!.interceptKey('BAROMETRIC', true); + this.keyEventManager!.interceptKey('BAROMETRIC_STD_PRESSURE', true); this.keyEventSub = this.bus.getSubscriber().on('key_intercept').handle(this.onKeyIntercepted.bind(this)); } @@ -74,7 +77,11 @@ export class AltimeterBaroKeyEventHandler { private onKeyIntercepted(data: KeyEventData): void { switch (data.key) { case 'BAROMETRIC': { - this.bus.pub('baro_set', true, true, false); + this.bus.getPublisher().pub('baro_set', true, true, false); + break; + } + case 'BAROMETRIC_STD_PRESSURE': { + this.bus.getPublisher().pub('std_baro_switch', true, true, false); break; } } diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Input/ControlpadHEventHandler.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Input/ControlpadHEventHandler.ts new file mode 100644 index 000000000..191730c10 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Input/ControlpadHEventHandler.ts @@ -0,0 +1,121 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { FmsHEvent } from '../UI/FmsHEvent'; + +export enum ControlPadKeyOperations { + None, + InsertCharacter, + ApplyBackSpace, + ReverseSign, +} + +/** + * + */ +interface KeyboardEventTranslationResult { + /** The operation found by the evaluation */ + KeyboardOperation: ControlPadKeyOperations, + /** Pressed key as string, to be inserted e.g. into InputComponent */ + ReceivedKey: string | null +} + +/** + * Handling the raw H-events, coming from the control pad (e.g. of the SR22). + * This provides an abstraction for the 40 raw events, which are translated into three operations: + * - insert character + * - backspace + * - signReversal (+/-) + */ +export class ControlpadHEventHandler { + + private static prefetchedCharacter: string | undefined; + + private static readonly touchPadKeyMap = new Map([ + [FmsHEvent.A, 'A'], + [FmsHEvent.B, 'B'], + [FmsHEvent.C, 'C'], + [FmsHEvent.D, 'D'], + [FmsHEvent.E, 'E'], + [FmsHEvent.F, 'F'], + [FmsHEvent.G, 'G'], + [FmsHEvent.H, 'H'], + [FmsHEvent.I, 'I'], + [FmsHEvent.J, 'J'], + [FmsHEvent.K, 'K'], + [FmsHEvent.L, 'L'], + [FmsHEvent.M, 'M'], + [FmsHEvent.N, 'N'], + [FmsHEvent.O, 'O'], + [FmsHEvent.P, 'P'], + [FmsHEvent.Q, 'Q'], + [FmsHEvent.R, 'R'], + [FmsHEvent.S, 'S'], + [FmsHEvent.T, 'T'], + [FmsHEvent.U, 'U'], + [FmsHEvent.V, 'V'], + [FmsHEvent.W, 'W'], + [FmsHEvent.X, 'X'], + [FmsHEvent.Y, 'Y'], + [FmsHEvent.Z, 'Z'], + [FmsHEvent.SPC, ' '], + [FmsHEvent.D0, '0'], + [FmsHEvent.D1, '1'], + [FmsHEvent.D2, '2'], + [FmsHEvent.D3, '3'], + [FmsHEvent.D4, '4'], + [FmsHEvent.D5, '5'], + [FmsHEvent.D6, '6'], + [FmsHEvent.D7, '7'], + [FmsHEvent.D8, '8'], + [FmsHEvent.D9, '9'], + [FmsHEvent.Dot, '.'], + ]); + + + /** + * Checks an FmsHEvent whether it represents a character key from the controlpad keyboard + * @param evt FmsHEvent, which needs to be checked + * @returns boolean result of check + */ + public static isKeyboardTextInput(evt: FmsHEvent): boolean { + return this.touchPadKeyMap.has(evt); + } + + /** + * Translate an FmsHEvent into one of the keyboard operations insert character, backspace or +/- + * @param evt FmsHEvent, which needs to be translated + * @returns the found keyboard input operation and the key as string argument if available + */ + public static evaluateKeyboardInput(evt: FmsHEvent): KeyboardEventTranslationResult { + const containedCharacter = this.touchPadKeyMap.get(evt); + if (containedCharacter !== undefined) { + ControlpadHEventHandler.prefetchedCharacter = containedCharacter; + return { KeyboardOperation: ControlPadKeyOperations.InsertCharacter, ReceivedKey: containedCharacter }; + } else { + switch (evt) { + case FmsHEvent.BKSP: + return { KeyboardOperation: ControlPadKeyOperations.ApplyBackSpace, ReceivedKey: null }; + case FmsHEvent.PlusMinus: + return { KeyboardOperation: ControlPadKeyOperations.ReverseSign, ReceivedKey: null }; + default: + return { KeyboardOperation: ControlPadKeyOperations.None, ReceivedKey: null }; + } + } + } + + /** + * This static method returns the character, which was evaluated the last time, when evaluateKeyboardInput was called. + * @returns prefetched character. + */ + public static getPrefetchedCharacter(): string | undefined { + const prefetchedCharacter = ControlpadHEventHandler.prefetchedCharacter; + ControlpadHEventHandler.prefetchedCharacter = undefined; + return prefetchedCharacter; + } + + /** This static method clears the character, which was evaluated when evaluateKeyboardInput was called the last time. */ + public static clearPrefetchedCharacter(): void { + ControlpadHEventHandler.prefetchedCharacter = undefined; + } + +} + diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Map/Indicators/MapRelativeTerrainStatusIndicator.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Map/Indicators/MapRelativeTerrainStatusIndicator.css index 3e2b51967..898ebc4db 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Map/Indicators/MapRelativeTerrainStatusIndicator.css +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Map/Indicators/MapRelativeTerrainStatusIndicator.css @@ -9,6 +9,7 @@ position: relative; width: var(--map-rel-terrain-status-icon-width, 1.3em); height: var(--map-rel-terrain-status-icon-height, 1.2em); + overflow: hidden; } .map-rel-terrain-status-icon { @@ -25,6 +26,7 @@ top: 0px; width: 100%; height: 100%; + overflow: visible; } .map-rel-terrain-status-failed-cross { diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Map/MapUserSettings.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Map/MapUserSettings.ts index 058565530..895e19b52 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Map/MapUserSettings.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Map/MapUserSettings.ts @@ -92,6 +92,10 @@ export class MapUserSettings { name: 'mapAutoNorthUpRangeIndex', defaultValue: 27 }, + { + name: 'mapGroundNorthUpActive', + defaultValue: false + }, { name: 'mapPfdDeclutter', defaultValue: MapDeclutterSettingMode.All diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComFrequencyElement.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComFrequencyElement.tsx index 26d3bc485..43f9fda0c 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComFrequencyElement.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComFrequencyElement.tsx @@ -1,12 +1,15 @@ /* eslint-disable max-len */ // eslint-disable-next-line @typescript-eslint/no-unused-vars import { - ComponentProps, DisplayComponent, EventBus, FrequencyBank, FrequencyChangeEvent, FSComponent, IdentChangeEvent, NavEvents, NavSourceId, NavSourceType, Radio, - RadioEvents, RadioType, VNode + ComponentProps, ControlPublisher, EventBus, FrequencyBank, FrequencyChangeEvent, FSComponent, IdentChangeEvent, NavComEvents, NavEvents, NavSourceId, NavSourceType, Radio, + RadioEvents, RadioType, VNode, } from '@microsoft/msfs-sdk'; +import { ComRadioSpacingSettingMode, ComRadioUserSettings } from '@microsoft/msfs-garminsdk'; + import { AvionicsComputerSystemEvents } from '../Systems/AvionicsComputerSystem'; import { AvionicsSystemState, AvionicsSystemStateEvent } from '../Systems/G1000AvionicsSystem'; +import { FmsHEvent, G1000UiControl } from '../UI'; import './NavComFrequencyElement.css'; @@ -27,15 +30,56 @@ interface NavComFrequencyElementProps extends ComponentProps { /** * Representation of the active and standby frequencies of a nav or com radio. */ -export class NavComFrequencyElement extends DisplayComponent { +export class NavComFrequencyElement extends G1000UiControl { + private readonly controlPadFrequencyInputMap: Map = new Map([ + [FmsHEvent.D0, 0], + [FmsHEvent.D1, 1], + [FmsHEvent.D2, 2], + [FmsHEvent.D3, 3], + [FmsHEvent.D4, 4], + [FmsHEvent.D5, 5], + [FmsHEvent.D6, 6], + [FmsHEvent.D7, 7], + [FmsHEvent.D8, 8], + [FmsHEvent.D9, 9], + [FmsHEvent.Dot, -1], + ]); + + + private containerRef = FSComponent.createRef(); private selectorBorderElement = FSComponent.createRef(); private selectorArrowElement = FSComponent.createRef(); private activeFreq = FSComponent.createRef(); private standbyFreq = FSComponent.createRef(); + + private comInputDigit0 = FSComponent.createRef(); + private comInputDigit1 = FSComponent.createRef(); + private comInputDigit2 = FSComponent.createRef(); + private comInputDigit3 = FSComponent.createRef(); + private comInputDigit4 = FSComponent.createRef(); + private comInputDigit5 = FSComponent.createRef(); + private comInputDigit6 = FSComponent.createRef(); + + private comInputDigits = (this.props.type === RadioType.Com) ? + [this.comInputDigit0, this.comInputDigit1, this.comInputDigit2, this.comInputDigit3, this.comInputDigit4, this.comInputDigit5, this.comInputDigit6] : + [this.comInputDigit0, this.comInputDigit1, this.comInputDigit2, this.comInputDigit3, this.comInputDigit4, this.comInputDigit5]; + private ident = FSComponent.createRef(); private isFailed = false; + private selected = false; + + private isInInputMode = false; + private radioState: Radio | undefined; + + private digitPosition = 0; + private previousFrequency = 0; + private newFrequencyAsString = ''; + private validNextDigitSpace = [1]; + + private readonly comRadioSettingManager = ComRadioUserSettings.getManager(this.props.bus); + private readonly controlPublisher = new ControlPublisher(this.props.bus); /** * Set this frequency as the active selection visually. @@ -69,6 +113,18 @@ export class NavComFrequencyElement extends DisplayComponent() .on('avionicscomputer_state_2') .handle(this.onComputerStateChanged.bind(this)); + + this.standbyFreq.instance.style.display = ''; + this.comInputDigits.every(digit => { + digit.instance.style.display = 'none'; + return true; + }); + + if (this.props.type === RadioType.Com) { + this.props.bus.getSubscriber().on(`com_transmit_${this.props.index === 1 ? 1 : 2}`).handle(this.onComTransmitChange.bind(this)); + } + + this.controlPublisher.startPublish(); } /** @@ -111,6 +167,9 @@ export class NavComFrequencyElement extends DisplayComponent { - if (this.ident.getOrDefault() !== null) { + if (this.ident.getOrDefault() !== null && this.selected) { if (strength == 0) { if (this.ident.instance.style.display !== 'none') { this.ident.instance.style.display = 'none'; @@ -196,6 +258,244 @@ export class NavComFrequencyElement extends DisplayComponent { + this.activeFreq.instance.classList.toggle('transmit-selected', transmitting); + }; + + /** @inheritdoc */ + public onInteractionEvent(evt: FmsHEvent): boolean { + let isHandled = false; + if (this.radioState?.selected) { + if ([FmsHEvent.D0, FmsHEvent.D1, FmsHEvent.D2, FmsHEvent.D3, FmsHEvent.D4, FmsHEvent.D5, FmsHEvent.D6, FmsHEvent.D7, FmsHEvent.D8, FmsHEvent.D9, FmsHEvent.Dot].includes(evt)) { + // Digit handling: + if (this.isInInputMode === false) { + // We are entering the data input mode, set up the required variables: + this.startFrequencyEntry(); + } + this.handleFrequencyEntry(evt); + isHandled = true; + } else { + // All other events terminate the input mode, canceling the captured input with the sole exception of ENT: + this.stopFrequencyEntry(); + if (evt === FmsHEvent.ENT) { + // ENT handling, publish the respective freq change bus event: + this.controlPublisher.publishEvent(this.radioState.radioType === RadioType.Com ? 'standby_com_freq' : 'standby_nav_freq', this.newFrequencyAsString); + } else { + // All other events shall cancel the input and restore the previous frequncy: + this.onUpdateFrequency({ radio: this.radioState, bank: FrequencyBank.Standby, frequency: this.previousFrequency }); + } + } + } + return isHandled; + } + + /** Begins frequency entry phase */ + private startFrequencyEntry(): void { + this.isInInputMode = true; + this.validNextDigitSpace = [1]; + this.digitPosition = 0; + this.newFrequencyAsString = this.radioState?.standbyFrequency.toFixed((this.radioState.radioType === RadioType.Com) ? 3 : 2) ?? '000.00'; + this.previousFrequency = this.radioState?.standbyFrequency ?? 0; + + // ...and set up the UI as needed (swap the input digits for the static frequency element): + this.standbyFreq.instance.style.display = 'none'; + this.comInputDigits.every((digit, index) => { + digit.instance.style.display = ''; + digit.instance.textContent = this.newFrequencyAsString[index]; + digit.instance.classList.remove('highlight-select'); + return true; + }); + this.comInputDigits[this.digitPosition].instance.classList.add('highlight-select'); + } + + /** Begins frequency entry phase */ + private stopFrequencyEntry(): void { + this.isInInputMode = false; + + // ...and set up the UI as needed: + this.standbyFreq.instance.style.display = ''; + this.comInputDigits.every((digit) => { + digit.instance.classList.remove('highlight-select'); + digit.instance.style.display = 'none'; + return true; + }); + } + + /** + * Handles the frequency input event + * @param evt received hEvent + */ + private handleFrequencyEntry(evt: FmsHEvent): void { + // Here we handle the input of digits for a radio frequency. + // Fetch the digit from the event first: + const digit = this.controlPadFrequencyInputMap.get(evt); + if (digit !== undefined) { + // We received a valid input for frequency input: + if (this.validNextDigitSpace.includes(digit)) { + const digitAsString = digit.toFixed(0); + switch (this.digitPosition) { + case 0: + this.newFrequencyAsString = digitAsString + this.newFrequencyAsString.substring(1); + break; + case 1: + this.newFrequencyAsString = this.newFrequencyAsString.substring(0, 1) + digitAsString + this.newFrequencyAsString.substring(2); + break; + case 2: + this.newFrequencyAsString = this.newFrequencyAsString.substring(0, 2) + digitAsString + this.newFrequencyAsString.substring(3); + this.digitPosition++; // Skip the dot + break; + case 4: + if (digit < 0) { + // We received the dot while on pos 4, stay on pos 4 and expect the digit the next time: + this.newFrequencyAsString = this.newFrequencyAsString.substring(0, 3) + '.' + this.newFrequencyAsString.substring(4); + this.digitPosition--; + } else { + // We received the digit at pos 4, auto fill in the dot: + this.newFrequencyAsString = this.newFrequencyAsString.substring(0, 3) + '.' + digitAsString + this.newFrequencyAsString.substring(5); + } + break; + case 5: + this.newFrequencyAsString = this.newFrequencyAsString.substring(0, 5) + digitAsString + this.newFrequencyAsString.substring(6); + break; + case 6: + this.newFrequencyAsString = this.newFrequencyAsString.substring(0, 6) + digitAsString; + break; + } + if (this.digitPosition < 6) { + this.validNextDigitSpace = this.getNextValidFrequencyDigits(digit); + this.digitPosition++; + } else { + // After position 6, we don't consider any further digit as valid: + this.validNextDigitSpace = []; + } + // Update the new frequency on the UI: + this.comInputDigits.every((inputDigit, index) => { + inputDigit.instance.textContent = this.newFrequencyAsString[index]; + inputDigit.instance.classList.remove('highlight-select'); + return true; + }); + this.comInputDigits[this.digitPosition].instance.classList.add('highlight-select'); + } + } + } + + /** + * Returns the valid digits at the next digit position for COM entry mode + * @param digit Received digit at the current position. + * @returns the valid numbers when entering the next com frequency digit + */ + private getNextValidComDigits(digit: number): number[] { + // For COM the valid range is 118.000 - 136.990: + switch (this.digitPosition) { + case 0: + // The second digit can be: + return [1, 2, 3]; + + case 1: + switch (digit) { + case 1: + // If the second digit is 1, the third digit can be: + return [8, 9]; + case 2: + // If the second digit is 2, the third digit can be: + return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + case 3: + // If the second digit is 3, the third digit can be: + return [0, 1, 2, 3, 4, 5, 6]; + } + break; + + case 2: + case 3: + // After the dot, any digit is valid for the fourth digit: + return [- 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + case 4: + if (this.comRadioSettingManager.getSetting('comRadioSpacing').get() === ComRadioSpacingSettingMode.Spacing25Khz) { + // If the spacing is 0.025, the sixth digit can be: + return [0, 2, 5, 7]; + } else { + // If the spacing is 0.00833, the sixth digit can be: + return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + } + + case 5: + if (this.comRadioSettingManager.getSetting('comRadioSpacing').get() === ComRadioSpacingSettingMode.Spacing25Khz) { + // If the spacing is 0.025, the seventh digit can be: + switch (digit) { + case 0: + // If the sixth digit is 0, the seventh digit can only be: + return [0]; + case 2: + // If the sixth digit is 2, the seventh digit can only be: + return [5]; + case 5: + // If the sixth digit is 5, the seventh digit can only be: + return [0]; + case 7: + // If the sixth digit is 7, the seventh digit can only be: + return [5]; + } + break; + } + } + return []; + } + + /** + * Returns the valid digits at the next digit position for NAV entry mode + * @param digit Received digit at the current position. + * @returns the valid numbers when entering the next nav frequency digit + */ + getNextValidNavDigits(digit: number): number[] { + switch (this.digitPosition) { + case 0: + // The second digit can be: + return [0, 1]; + + case 1: + switch (digit) { + case 0: + // If the second digit is 0, the third digit can be: + return [8, 9]; + case 1: + // If the second digit is 1, the third digit can be: + return [0, 1, 2, 3, 4, 5, 6, 7]; + } + break; + + case 2: + // The fourth digit always needs to be the dot: + return [-1]; // Next we expect the dot + + case 3: + // After the dot, any digit is valid for the fifth digit: + return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + case 4: + // The sixth digit can be: + return [0, 5]; + + case 5: + // The seventh digit can only be: + return [0]; + } + return []; + } + + /** + * Evaluate the valid digits at the next digit position, based on COM vs NAV and the spacing + * @param digit Received digit at the current position. + * @returns the valid numbers when entering the next digit, based on digit position: + */ + private getNextValidFrequencyDigits(digit: number): number[] { + if (this.radioState?.radioType === RadioType.Com) { + return this.getNextValidComDigits(digit); + } else { + return this.getNextValidNavDigits(digit); + } + } + /** * Render NavCom Freq Element. * @returns Vnode containing the element. @@ -208,6 +508,12 @@ export class NavComFrequencyElement extends DisplayComponent + 1 + 0 + 0 + . + 1 + 2 @@ -231,6 +537,13 @@ export class NavComFrequencyElement extends DisplayComponent + 1 + 0 + 0 + . + 1 + 2 + 0
); diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComRadio.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComRadio.css index 9fa68b1a0..15aef1aec 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComRadio.css +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComRadio.css @@ -70,4 +70,54 @@ left: 108px; width: 88px; height: 24px; -} \ No newline at end of file +} + +.radio-armed-border { + border: solid 2px cyan; + position: absolute; + top: 1.5px; + width: 100%; + height: 100%; +} + +.radio-armed-border.com { + border-bottom-left-radius: 10px; + left: -1.5px; +} + +.radio-armed-border.nav { + border-bottom-right-radius: 10px; + right: -1.5px; +} + +.radio-armed-border.inactive { + opacity: 0; +} + +.radio-armed-border.blink { + animation: ArmedFade 1s infinite; +} + +.radio-armed-border.solid { + opacity: 1; +} + +.radio-armed-border.standby { + opacity: 0; +} + +@keyframes ArmedFade { + + 0%, + 50% { + opacity: 1; + } + + 75% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComRadio.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComRadio.tsx index e452757a5..ce406d633 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComRadio.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/NavCom/NavComRadio.tsx @@ -1,6 +1,7 @@ /* eslint-disable max-len */ -import { ComponentProps, DisplayComponent, EventBus, FSComponent, RadioType, VNode } from '@microsoft/msfs-sdk'; +import { ClockEvents, ComponentProps, ConsumerSubject, EventBus, FSComponent, RadioType, Subject, VNode } from '@microsoft/msfs-sdk'; +import { FmsHEvent, G1000UiControl } from '../UI'; import { NavComFrequencyElement } from './NavComFrequencyElement'; import './NavComRadio.css'; @@ -22,19 +23,102 @@ interface NavComRadioProps extends ComponentProps { templateId: string; } +enum ArmedModes { + inactive = 'inactive', + blink = 'blink', + solid = 'solid', + standby = 'standby', +} + /** * */ -export class NavComRadio extends DisplayComponent { +export class NavComRadio extends G1000UiControl { + frequency1Element = FSComponent.createRef(); frequency2Element = FSComponent.createRef(); + private readonly sub = this.props.bus.getSubscriber(); + private readonly simTime = ConsumerSubject.create(this.sub.on('simTime').withPrecision(-2), 0); // milliseconds, updates at 10Hz + private activeBorderRef = FSComponent.createRef(); + public armedMode = Subject.create(ArmedModes.inactive); + private lastArmedTime = 0; // milliseconds + private readonly blinkTimeout = 5; // seconds + private readonly solidTimeout = 10; // seconds + private readonly borderClasses: ArmedModes[] = [ + ArmedModes.inactive, + ArmedModes.blink, + ArmedModes.solid, + ArmedModes.standby + ]; + + /** * Stuff to do after render. */ public onAfterRender(): void { - // Nothing to do at the moment. - return; + + // Add and remove CSS classes based on changes in ArmedMode + this.armedMode.sub((mode) => { + + // Find class to add + const addClassIndex = this.borderClasses.indexOf(mode); + if (addClassIndex < 0) { return; } + const addClass = this.borderClasses[addClassIndex]; + + // Find classes to remove + const removeClasses = []; + for (let i = 0; i < this.borderClasses.length; i++) { + if (i !== addClassIndex) { + removeClasses.push(this.borderClasses[i]); + } + } + if (removeClasses.length !== this.borderClasses.length - 1) { return; } + + // Set last armed time + if (mode === ArmedModes.blink) { this.lastArmedTime = this.simTime.get(); } + + // Add and remove classes + this.activeBorderRef.instance.classList.add(addClass); + this.activeBorderRef.instance.classList.remove( + removeClasses[0], + removeClasses[1], + removeClasses[2], + ); + }, true); + + // Watch for blink and solid border timeouts + this.simTime.sub((t) => { + if (this.armedMode.get() === ArmedModes.blink && (t - this.lastArmedTime > this.blinkTimeout * 1000)) { + this.armedMode.set(ArmedModes.solid); + } + if (this.armedMode.get() === ArmedModes.solid && (t - this.lastArmedTime > this.solidTimeout * 1000)) { + this.armedMode.set(ArmedModes.standby); + } + }, true); + + // Initilize the COM radio in standby mode + if (this.props.position === 'right') { + this.armedMode.set(ArmedModes.standby); + } + } + + /** @inheritdoc */ + public onInteractionEvent(evt: FmsHEvent): boolean { + // We simply pass on the event to the frequency elements: + return this.frequency1Element.instance.onInteractionEvent(evt) || + this.frequency2Element.instance.onInteractionEvent(evt); + } + + /** Sets the armed mode of the radio input box + * @param armed whether or not the radio box is armed for input + */ + public setArmed(armed: boolean): void { + if (armed) { + this.armedMode.set(ArmedModes.blink); + } else { + this.armedMode.set(ArmedModes.inactive); + } } /** @@ -51,6 +135,7 @@ export class NavComRadio extends DisplayComponent {
+ ); } else { @@ -62,8 +147,9 @@ export class NavComRadio extends DisplayComponent {
+
); } } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/AirspeedIndicatorConfig.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/AirspeedIndicatorConfig.ts new file mode 100644 index 000000000..f873cb7d1 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/AirspeedIndicatorConfig.ts @@ -0,0 +1,389 @@ +import { SubscribableMapFunctions, SubscribableUtils } from '@microsoft/msfs-sdk'; + +import { + AirspeedDefinitionFactory, AirspeedIndicatorBottomDisplayMode, AirspeedIndicatorBottomDisplayOptions, AirspeedIndicatorColorRange, + AirspeedIndicatorColorRangeColor, AirspeedIndicatorColorRangeWidth, AirspeedIndicatorDataProviderOptions, AirspeedTapeScaleOptions +} from '@microsoft/msfs-garminsdk'; + +import { Config, ConfigFactory } from '../../Config/Config'; +import { NumericConfig } from '../../Config/NumericConfig'; +import { ColorRangeConfig } from './ColorRangeConfig'; +import { VSpeedBugConfig } from './VSpeedBugConfig'; + +/** + * A configuration object which defines airspeed indicator options. + */ +export class AirspeedIndicatorConfig implements Config { + private static readonly DEFAULT_DATA_OPTIONS: Readonly = { + trendLookahead: 6, + overspeedThreshold: (): number => { + const vne = Simplane.getDesignSpeeds().VNe; + return vne > 60 ? vne : Number.POSITIVE_INFINITY; + }, + underspeedThreshold: (): number => Simplane.getDesignSpeeds().VS0 + }; + + private static readonly DEFAULT_TAPE_SCALE_OPTIONS: Readonly = { + minimum: 20, + maximum: 999, + window: 60, + majorTickInterval: 10, + minorTickFactor: 2 + }; + + private static readonly DEFAULT_BOTTOM_OPTIONS: Readonly = { + mode: AirspeedIndicatorBottomDisplayMode.TrueAirspeed, + machThreshold: undefined + }; + + /** Options for the airspeed indicator data provider. */ + public readonly dataProviderOptions: Readonly; + + /** Options for the airspeed tape scale. */ + public readonly tapeScaleOptions: Readonly; + + /** Color range definitions for the airspeed tape. */ + public readonly colorRangeDefinitions: readonly AirspeedIndicatorColorRange[]; + + /** Options for the bottom display box. */ + public readonly bottomDisplayOptions: Readonly; + + /** Reference V-speed bug config options for the airspeed indicator. */ + public readonly vSpeedBugConfigs: readonly VSpeedBugConfig[]; + + /** + * Creates a new AirspeedIndicatorConfig from a configuration document element. + * @param element A configuration document element. + * @param factory A configuration object factory to use to create child configuration objects. + */ + constructor(element: Element | undefined, factory: ConfigFactory) { + if (element === undefined) { + this.dataProviderOptions = { ...AirspeedIndicatorConfig.DEFAULT_DATA_OPTIONS }; + this.tapeScaleOptions = { ...AirspeedIndicatorConfig.DEFAULT_TAPE_SCALE_OPTIONS }; + this.colorRangeDefinitions = this.getDefaultColorRangeDefinitions(this.tapeScaleOptions); + this.bottomDisplayOptions = { ...AirspeedIndicatorConfig.DEFAULT_BOTTOM_OPTIONS }; + this.vSpeedBugConfigs = this.getDefaultVSpeedBugOptions(); + } else { + if (element.tagName !== 'AirspeedIndicator') { + throw new Error(`Invalid AirspeedIndicatorConfig definition: expected tag name 'AirspeedIndicator' but was '${element.tagName}'`); + } + + try { + this.dataProviderOptions = this.parseDataProviderOptions(element, factory); + } catch (e) { + console.warn(e); + this.dataProviderOptions = { ...AirspeedIndicatorConfig.DEFAULT_DATA_OPTIONS }; + } + + try { + this.tapeScaleOptions = this.parseScaleOptions(element); + } catch (e) { + console.warn(e); + this.tapeScaleOptions = { ...AirspeedIndicatorConfig.DEFAULT_TAPE_SCALE_OPTIONS }; + } + + try { + this.colorRangeDefinitions = this.parseColorRangeDefinitions(element, factory, this.tapeScaleOptions); + } catch (e) { + console.warn(e); + this.colorRangeDefinitions = this.getDefaultColorRangeDefinitions(this.tapeScaleOptions); + } + + try { + this.bottomDisplayOptions = this.parseBottomDisplayOptions(element); + } catch (e) { + console.warn(e); + this.bottomDisplayOptions = { ...AirspeedIndicatorConfig.DEFAULT_BOTTOM_OPTIONS }; + } + + try { + this.vSpeedBugConfigs = this.parseVSpeedBugOptions(element); + } catch (e) { + console.warn(e); + this.vSpeedBugConfigs = this.getDefaultVSpeedBugOptions(); + } + } + } + + /** + * Parses data provider options from a configuration document element. + * @param element A configuration document element. + * @param factory A configuration object factory to use to create child configuration objects. + * @returns Data provider options defined by the specified element. + * @throws Error if the specified element has an invalid format. + */ + private parseDataProviderOptions(element: Element, factory: ConfigFactory): AirspeedIndicatorDataProviderOptions { + const options: Partial = {}; + + const trendVector = element.querySelector(':scope>TrendVector'); + if (trendVector !== null) { + const trendLookahead = Number(trendVector.getAttribute('lookahead') ?? undefined); + + if (isFinite(trendLookahead) && trendLookahead > 0) { + options.trendLookahead = trendLookahead; + } else { + console.warn('Invalid AirspeedIndicatorConfig definition: unrecognized trend vector lookahead option (must be a positive number). Defaulting to 6.'); + } + } + + const alerts = element.querySelector(':scope>SpeedAlerts'); + + if (alerts !== null) { + const overspeed = alerts.querySelector(':scope>Overspeed'); + options.overspeedThreshold = this.parseSpeedAlertThreshold(overspeed, factory); + + const underspeed = alerts.querySelector(':scope>Underspeed'); + options.underspeedThreshold = this.parseSpeedAlertThreshold(underspeed, factory); + } + + options.trendLookahead ??= AirspeedIndicatorConfig.DEFAULT_DATA_OPTIONS.trendLookahead; + options.overspeedThreshold ??= AirspeedIndicatorConfig.DEFAULT_DATA_OPTIONS.overspeedThreshold; + options.underspeedThreshold ??= AirspeedIndicatorConfig.DEFAULT_DATA_OPTIONS.underspeedThreshold; + + return options as AirspeedIndicatorDataProviderOptions; + } + + /** + * Parses a factory for a speed alert threshold airspeed value from a configuration document element. + * @param element A configuration document element. + * @param factory A configuration object factory to use to create child configuration objects. + * @returns A factory for a speed alert threshold airspeed value defined by the specified element, or `undefined` if + * `element` is `undefined`. + * @throws Error if the specified element has an invalid format. + */ + private parseSpeedAlertThreshold(element: Element | null, factory: ConfigFactory): AirspeedDefinitionFactory | undefined { + const child = element?.children[0] ?? undefined; + if (child === undefined) { + return undefined; + } + + try { + const config = factory.create(child); + + if (config === undefined || !('isNumericConfig' in config)) { + console.warn('Invalid AirspeedIndicatorConfig definition: speed alert threshold is not a numeric config'); + return undefined; + } + + return (config as NumericConfig).resolve(); + } catch (e) { + console.warn(e); + } + + return undefined; + } + + /** + * Parses tape scale options from a configuration document element. + * @param element A configuration document element. + * @returns Tape scale options defined by the specified element. + * @throws Error if the specified element has an invalid format. + */ + private parseScaleOptions(element: Element): AirspeedTapeScaleOptions { + const scale = element.querySelector(':scope>Scale'); + + if (scale === null) { + return { ...AirspeedIndicatorConfig.DEFAULT_TAPE_SCALE_OPTIONS }; + } + + const scaleOptions: Partial = { + minimum: undefined as number | undefined, + maximum: undefined as number | undefined, + window: undefined as number | undefined, + majorTickInterval: undefined as number | undefined, + minorTickFactor: undefined as number | undefined + }; + + scaleOptions.minimum = Number(scale.getAttribute('min') ?? 'NaN'); + scaleOptions.maximum = Number(scale.getAttribute('max') ?? 'NaN'); + scaleOptions.window = Number(scale.getAttribute('window') ?? 'NaN'); + scaleOptions.majorTickInterval = Number(scale.getAttribute('major-tick-interval') ?? 'NaN'); + scaleOptions.minorTickFactor = Number(scale.getAttribute('minor-tick-factor') ?? 'NaN'); + + for (const key in scaleOptions) { + const typedKey = key as keyof AirspeedTapeScaleOptions; + + if (isNaN(scaleOptions[typedKey] as number)) { + console.warn(`Invalid AirspeedIndicatorConfig definition: invalid scale option '${key}'`); + scaleOptions[typedKey] = undefined; + } + + if (scaleOptions[typedKey] === undefined) { + scaleOptions[typedKey] = AirspeedIndicatorConfig.DEFAULT_TAPE_SCALE_OPTIONS[typedKey]; + } + } + + return scaleOptions as AirspeedTapeScaleOptions; + } + + /** + * Parses color range definitions from a configuration document element. + * @param element A configuration document element. + * @param factory A configuration object factory to use to create child configuration objects. + * @param tapeScaleOptions Options describing the airspeed tape scale. + * @returns Color range definitions defined by the specified element. + */ + private parseColorRangeDefinitions(element: Element, factory: ConfigFactory, tapeScaleOptions: Readonly): readonly AirspeedIndicatorColorRange[] { + const colorRanges = element.querySelector(':scope>ColorRanges'); + if (colorRanges === null) { + return this.getDefaultColorRangeDefinitions(tapeScaleOptions); + } else { + return Array.from(colorRanges.querySelectorAll(':scope>ColorRange')).map(colorRangeElement => { + try { + return new ColorRangeConfig(colorRangeElement, factory).resolve(); + } catch (e) { + console.warn(e); + return null; + } + }).filter(def => def !== null) as readonly AirspeedIndicatorColorRange[]; + } + } + + /** + * Gets a default set of color range definitions. The set includes the following ranges (in order): + * 1. RED: Flaps extended stall range (tape minimum to Vs0). + * 2. WHITE: Flaps extended operating range (Vs0 to Vfe). + * 3. GREEN (half): Flaps extended or retracted operating range (Vs1 to Vfe). + * 4. GREEN (full): Flaps retracted operating range (Vfe to Vno). + * 5. YELLOW: Overspeed caution range (Vno to Vne). + * 6. BARBER POLE: Overspeed range (Vne to tape maximum). + * @param tapeScaleOptions Options describing the airspeed tape scale. + * @returns A array containing a default set of color range definitions. + */ + private getDefaultColorRangeDefinitions(tapeScaleOptions: Readonly): AirspeedIndicatorColorRange[] { + return [ + // Flaps extended stall range + { + width: AirspeedIndicatorColorRangeWidth.Full, + color: AirspeedIndicatorColorRangeColor.Red, + minimum: () => SubscribableUtils.isSubscribable(tapeScaleOptions.minimum) + ? tapeScaleOptions.minimum.map(SubscribableMapFunctions.identity()) + : tapeScaleOptions.minimum, + maximum: () => Simplane.getDesignSpeeds().VS0 + }, + + // Flaps extended operating range + { + width: AirspeedIndicatorColorRangeWidth.Full, + color: AirspeedIndicatorColorRangeColor.White, + minimum: () => Simplane.getDesignSpeeds().VS0, + maximum: () => Simplane.getDesignSpeeds().VFe + }, + + // Flaps retracted/extended operating range + { + width: AirspeedIndicatorColorRangeWidth.Half, + color: AirspeedIndicatorColorRangeColor.Green, + minimum: () => Simplane.getDesignSpeeds().VS1, + maximum: () => Simplane.getDesignSpeeds().VFe + }, + + // Flaps retracted operating range + { + width: AirspeedIndicatorColorRangeWidth.Full, + color: AirspeedIndicatorColorRangeColor.Green, + minimum: () => Simplane.getDesignSpeeds().VFe, + maximum: () => Simplane.getDesignSpeeds().VNo + }, + + // Overspeed caution range + { + width: AirspeedIndicatorColorRangeWidth.Full, + color: AirspeedIndicatorColorRangeColor.Yellow, + minimum: () => Simplane.getDesignSpeeds().VNo, + maximum: () => Simplane.getDesignSpeeds().VNe + }, + + // Barber pole + { + width: AirspeedIndicatorColorRangeWidth.Full, + color: AirspeedIndicatorColorRangeColor.BarberPole, + minimum: () => Simplane.getDesignSpeeds().VNe, + maximum: () => SubscribableUtils.isSubscribable(tapeScaleOptions.maximum) + ? tapeScaleOptions.maximum.map(SubscribableMapFunctions.identity()) + : tapeScaleOptions.maximum + } + ]; + } + + /** + * Parses bottom display box options from a configuration document element. + * @param element A configuration document element. + * @returns Bottom display box options defined by the specified element. + * @throws Error if the specified element has an invalid format. + */ + private parseBottomDisplayOptions(element: Element): AirspeedIndicatorBottomDisplayOptions { + const bottomDisplay = element.querySelector(':scope>BottomDisplay'); + + if (bottomDisplay === null) { + return { ...AirspeedIndicatorConfig.DEFAULT_BOTTOM_OPTIONS }; + } else { + let mode = bottomDisplay.getAttribute('mode'); + switch (mode) { + case AirspeedIndicatorBottomDisplayMode.TrueAirspeed: + case AirspeedIndicatorBottomDisplayMode.Mach: + break; + case null: + mode = AirspeedIndicatorBottomDisplayMode.TrueAirspeed; + break; + default: + console.warn(`Invalid AirspeedIndicatorConfig definition: unrecognized bottom display mode '${mode}'. Defaulting to 'Tas'.`); + mode = AirspeedIndicatorBottomDisplayMode.TrueAirspeed; + } + + let machThreshold: number | undefined; + const machThresholdAttr = bottomDisplay.getAttribute('mach-threshold'); + if (machThresholdAttr !== null) { + machThreshold = Number(machThresholdAttr); + if (isNaN(machThreshold)) { + console.warn('Invalid AirspeedIndicatorConfig definition: invalid bottom display mach threshold option. Discarding value.'); + machThreshold = undefined; + } + } + + return { + mode: mode as AirspeedIndicatorBottomDisplayMode, + machThreshold + }; + } + } + + /** + * Parses reference V-speed bug configuration objects from a configuration document element. + * @param element A configuration document element. + * @returns Reference V-speed bug configuration objects defined by the specified element. + */ + private parseVSpeedBugOptions(element: Element): VSpeedBugConfig[] { + const vSpeedBugs = element.querySelector(':scope>VSpeedBugs'); + if (vSpeedBugs === null) { + return this.getDefaultVSpeedBugOptions(); + } else { + return Array.from(vSpeedBugs.querySelectorAll(':scope>Bug')).map(vSpeedBugElement => { + try { + return new VSpeedBugConfig(vSpeedBugElement); + } catch (e) { + console.warn(e); + return null; + } + }).filter(def => def !== null) as VSpeedBugConfig[]; + } + } + + /** + * Gets a default set of V-speed bug configuration objects. The set includes configurations for the following bugs + * (in order): + * 1. V-speed name: `glide`, Bug label: `G`. + * 2. V-speed name: `r`, Bug label: `R`. + * 3. V-speed name: `x`, Bug label: `X`. + * 4. V-speed name: `y`, Bug label: `Y`. + * @returns A array containing a default set of V-speed bug configuration objects. + */ + private getDefaultVSpeedBugOptions(): VSpeedBugConfig[] { + return [ + new VSpeedBugConfig('glide', 'G'), + new VSpeedBugConfig('r', 'R'), + new VSpeedBugConfig('x', 'X'), + new VSpeedBugConfig('y', 'Y') + ]; + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/ColorRangeConfig.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/ColorRangeConfig.ts new file mode 100644 index 000000000..87879a419 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/ColorRangeConfig.ts @@ -0,0 +1,103 @@ +import { AirspeedIndicatorColorRange, AirspeedIndicatorColorRangeColor, AirspeedIndicatorColorRangeWidth } from '@microsoft/msfs-garminsdk'; + +import { ConfigFactory, ResolvableConfig } from '../../Config/Config'; +import { NumericConfig } from '../../Config/NumericConfig'; + + +/** + * A configuration object which defines an airspeed tape color range. + */ +export class ColorRangeConfig implements ResolvableConfig { + public readonly isResolvableConfig = true; + + /** The width of this config's color range. */ + public readonly width: AirspeedIndicatorColorRangeWidth; + + /** The color of this config's color range. */ + public readonly color: AirspeedIndicatorColorRangeColor; + + /** The config which defines the minimum airspeed value of this config's color range. */ + public readonly minimum: NumericConfig; + + /** The config which defines the maximum airspeed value of this config's color range. */ + public readonly maximum: NumericConfig; + + /** + * Creates a new ColorRangeConfig from a configuration document element. + * @param element A configuration document element. + * @param factory A configuration object factory to use to create child configuration objects. + */ + constructor(element: Element, factory: ConfigFactory) { + if (element.tagName !== 'ColorRange') { + throw new Error(`Invalid ColorRangeConfig definition: expected tag name 'ColorRange' but was '${element.tagName}'`); + } + + const width = element.getAttribute('width'); + switch (width) { + case AirspeedIndicatorColorRangeWidth.Full: + case AirspeedIndicatorColorRangeWidth.Half: + this.width = width; + break; + default: + throw new Error(`Invalid ColorRangeConfig definition: unrecognized width '${width}'`); + } + + const color = element.getAttribute('color'); + switch (color) { + case AirspeedIndicatorColorRangeColor.Red: + case AirspeedIndicatorColorRangeColor.Yellow: + case AirspeedIndicatorColorRangeColor.White: + case AirspeedIndicatorColorRangeColor.Green: + case AirspeedIndicatorColorRangeColor.BarberPole: + this.color = color; + break; + default: + throw new Error(`Invalid ColorRangeConfig definition: unrecognized color '${color}'`); + } + + const minimum = this.parseEndpoint(element.querySelector(':scope>Minimum'), factory); + const maximum = this.parseEndpoint(element.querySelector(':scope>Maximum'), factory); + + if (minimum === undefined) { + throw new Error('Invalid ColorRangeConfig definition: minimum endpoint is not defined or is not numeric'); + } + if (maximum === undefined) { + throw new Error('Invalid ColorRangeConfig definition: maximum endpoint is not defined or is not numeric'); + } + + this.minimum = minimum; + this.maximum = maximum; + } + + /** + * Parses an endpoint numeric config from a configuration document element. + * @param element A configuration document element. + * @param factory A configuration object factory to use to create child configuration objects. + * @returns The numeric config defined by the specified element, or `undefined` if one could not be created. + */ + private parseEndpoint(element: Element | null, factory: ConfigFactory): NumericConfig | undefined { + const firstChild = element?.children[0]; + + if (firstChild === undefined) { + return undefined; + } + + const config = factory.create(firstChild); + + if (config === undefined || !('isNumericConfig' in config)) { + return undefined; + } + + return config as NumericConfig; + } + + /** @inheritdoc */ + public resolve(): AirspeedIndicatorColorRange { + return { + width: this.width, + color: this.color, + minimum: this.minimum.resolve(), + maximum: this.maximum.resolve() + }; + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/VSpeedBugConfig.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/VSpeedBugConfig.ts new file mode 100644 index 000000000..d147f3840 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/VSpeedBugConfig.ts @@ -0,0 +1,73 @@ +import { VSpeedBugDefinition } from '@microsoft/msfs-garminsdk'; + +import { ResolvableConfig } from '../../Config/Config'; +import { VSpeedDefinition } from '../../VSpeed/VSpeed'; + +/** + * A configuration object which defines an airspeed reference V-speed bug. + */ +export class VSpeedBugConfig implements ResolvableConfig<(vSpeedDefs: Iterable) => VSpeedBugDefinition | undefined> { + public readonly isResolvableConfig = true; + + /** The name of the reference V-speed associated with this config's speed bug. */ + public readonly name: string; + + /** The label of this config's speed bug. */ + public readonly label: string; + + /** + * Creates a new VSpeedBugConfig from a configuration document element. + * @param element A configuration document element. + */ + public constructor(element: Element); + /** + * Creates a new VSpeedBugConfig using a specified V-speed name and bug label. + * @param name The name of the V-speed. + * @param label The label of the V-speed bug. + */ + public constructor(name: string, label: string); + // eslint-disable-next-line jsdoc/require-jsdoc + public constructor(arg1: Element | string, arg2?: string) { + if (typeof arg1 === 'object') { + const element = arg1; + + if (element.tagName !== 'Bug') { + throw new Error(`Invalid VSpeedBugConfig definition: expected tag name 'Bug' but was '${element.tagName}'`); + } + + const name = element.getAttribute('name'); + if (name === null) { + throw new Error('Invalid VSpeedBugConfig definition: undefined name'); + } + this.name = name; + + const label = element.getAttribute('label'); + if (label === null) { + throw new Error('Invalid VSpeedBugConfig definition: undefined label'); + } + this.label = label; + } else { + this.name = arg1; + this.label = arg2 as string; + } + } + + /** @inheritdoc */ + public resolve(): (vSpeedDefs: Iterable) => VSpeedBugDefinition | undefined { + + return (vSpeedDefs: Iterable): VSpeedBugDefinition | undefined => { + for (const def of vSpeedDefs) { + if (def.name === this.name) { + return { + name: this.name, + label: this.label, + showOffscale: true, + showLegend: false + }; + } + } + + return undefined; + }; + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/index.ts new file mode 100644 index 000000000..543f5b87c --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/AirspeedIndicator/index.ts @@ -0,0 +1,3 @@ +export * from './AirspeedIndicatorConfig'; +export * from './ColorRangeConfig'; +export * from './VSpeedBugConfig'; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/Autopilot/AutopilotConfig.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/Autopilot/AutopilotConfig.ts new file mode 100644 index 000000000..9db823e93 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/Autopilot/AutopilotConfig.ts @@ -0,0 +1,266 @@ +import { Config } from '../../Config/Config'; + +/** + * Options for the autopilot ROL director. + */ +export type AutopilotRollOptions = { + /** The minimum supported bank angle, in degrees. */ + minBankAngle: number; + + /** The maximum supported bank angle, in degrees. */ + maxBankAngle: number; +}; + +/** + * Options for the autopilot HDG director. + */ +export type AutopilotHdgOptions = { + /** The maximum supported bank angle, in degrees. */ + maxBankAngle: number; +}; + +/** + * Options for the autopilot VOR director. + */ +export type AutopilotVorOptions = { + /** The maximum supported bank angle, in degrees. */ + maxBankAngle: number; +}; + +/** + * Options for the autopilot LOC director. + */ +export type AutopilotLocOptions = { + /** The maximum supported bank angle, in degrees. */ + maxBankAngle: number; +}; + +/** + * Options for the autopilot LNAV director. + */ +export type AutopilotLNavOptions = { + /** The maximum supported bank angle, in degrees. */ + maxBankAngle: number; +}; + +/** + * Options for the autopilot Low Bank Mode. + */ +export type AutopilotLowBankOptions = { + /** The maximum supported bank angle, in degrees. */ + maxBankAngle: number; +}; + +/** + * A configuration object which defines options related to the autopilot. + */ +export class AutopilotConfig implements Config { + private static readonly DEFAULT_ROLL_MIN_BANK_ANGLE = 6; + private static readonly DEFAULT_MAX_BANK_ANGLE = 25; + private static readonly DEFAULT_LOW_BANK_ANGLE = 15; + + /** + * Whether `AP_ALT_VAR_SET` events should be treated as `AP_ALT_VAR_INC`/`AP_ALT_VAR_DEC` events for compatibility + * with ModelBehaviors that transform the latter into the former. + */ + public readonly supportAltSelCompatibility: boolean; + + /** Options for the autopilot ROL director. */ + public readonly rollOptions: AutopilotRollOptions; + + /** Options for the autopilot HDG director. */ + public readonly hdgOptions: AutopilotHdgOptions; + + /** Options for the autopilot VOR director. */ + public readonly vorOptions: AutopilotVorOptions; + + /** Options for the autopilot LOC director. */ + public readonly locOptions: AutopilotLocOptions; + + /** Options for the autopilot GPS/FMS director. */ + public readonly lnavOptions: AutopilotLNavOptions; + + /** Options for the autopilot Low Bank Mode. */ + public readonly lowBankOptions: AutopilotLowBankOptions; + + /** Whether HDG sync mode is supported. */ + public readonly isHdgSyncModeSupported: boolean; + + /** + * Creates a new AutopilotConfig from a configuration document element. + * @param element A configuration document element. + */ + public constructor(element: Element | undefined) { + if (element === undefined) { + this.supportAltSelCompatibility = true; + + this.rollOptions = { minBankAngle: AutopilotConfig.DEFAULT_ROLL_MIN_BANK_ANGLE, maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + this.hdgOptions = { maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + this.vorOptions = { maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + this.locOptions = { maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + this.lnavOptions = { maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + this.lowBankOptions = { maxBankAngle: AutopilotConfig.DEFAULT_LOW_BANK_ANGLE }; + + this.isHdgSyncModeSupported = false; + } else { + if (element.tagName !== 'Autopilot') { + throw new Error(`Invalid AutopilotConfig definition: expected tag name 'Autopilot' but was '${element.tagName}'`); + } + + const supportAltSelCompatibility = element.getAttribute('alt-sel-compat')?.toLowerCase(); + switch (supportAltSelCompatibility) { + case 'true': + case undefined: + this.supportAltSelCompatibility = true; + break; + case 'false': + this.supportAltSelCompatibility = false; + break; + default: + console.warn(`Invalid AutopilotConfig definition: unrecognized alt-sel-compat option "${supportAltSelCompatibility}" (expected "true" or "false"). Defaulting to "false".`); + this.supportAltSelCompatibility = false; + } + + this.rollOptions = this.parseRollOptions(element.querySelector(':scope>ROL')); + this.hdgOptions = this.parseHdgOptions(element.querySelector(':scope>HDG')); + this.vorOptions = this.parseVorOptions(element.querySelector(':scope>VOR')); + this.locOptions = this.parseLocOptions(element.querySelector(':scope>LOC')); + this.lnavOptions = this.parseLNavOptions(element.querySelector(':scope>FMS')); + this.lowBankOptions = this.parseLowBankOptions(element.querySelector(':scope>LowBank')); + + const isHdgSyncModeSupported = element.getAttribute('hdg-sync-mode')?.toLowerCase(); + switch (isHdgSyncModeSupported) { + case 'true': + this.isHdgSyncModeSupported = true; + break; + case 'false': + case undefined: + this.isHdgSyncModeSupported = false; + break; + default: + console.warn(`Invalid AutopilotConfig definition: unrecognized hdg-sync-mode option "${isHdgSyncModeSupported}" (expected "true" or "false"). Defaulting to "false".`); + this.isHdgSyncModeSupported = false; + } + } + } + + /** + * Parses ROL director options from a configuration document element. + * @param element A configuration document element. + * @returns The ROL director options defined by the configuration document element. + */ + private parseRollOptions(element: Element | null): AutopilotRollOptions { + if (element !== null) { + let minBankAngle = Number(element.getAttribute('min-bank') ?? undefined); + if (isNaN(minBankAngle) || minBankAngle < 0) { + console.warn('Invalid AutopilotConfig definition: missing or unrecognized min-bank value (expected a non-negative number)'); + minBankAngle = AutopilotConfig.DEFAULT_ROLL_MIN_BANK_ANGLE; + } + + let maxBankAngle = Number(element.getAttribute('max-bank') ?? undefined); + if (isNaN(maxBankAngle) || maxBankAngle < 0) { + console.warn('Invalid AutopilotConfig definition: missing or unrecognized max-bank value (expected a non-negative number)'); + maxBankAngle = AutopilotConfig.DEFAULT_MAX_BANK_ANGLE; + } + + return { minBankAngle, maxBankAngle }; + } + + return { minBankAngle: AutopilotConfig.DEFAULT_ROLL_MIN_BANK_ANGLE, maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + } + + /** + * Parses HDG director options from a configuration document element. + * @param element A configuration document element. + * @returns The HDG director options defined by the configuration document element. + */ + private parseHdgOptions(element: Element | null): AutopilotHdgOptions { + if (element !== null) { + let maxBankAngle = Number(element.getAttribute('max-bank') ?? undefined); + if (isNaN(maxBankAngle) || maxBankAngle < 0) { + console.warn('Invalid AutopilotConfig definition: missing or unrecognized max-bank value (expected a non-negative number)'); + maxBankAngle = AutopilotConfig.DEFAULT_MAX_BANK_ANGLE; + } + + return { maxBankAngle }; + } + + return { maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + } + + /** + * Parses VOR director options from a configuration document element. + * @param element A configuration document element. + * @returns The VOR director options defined by the configuration document element. + */ + private parseVorOptions(element: Element | null): AutopilotVorOptions { + if (element !== null) { + let maxBankAngle = Number(element.getAttribute('max-bank') ?? undefined); + if (isNaN(maxBankAngle) || maxBankAngle < 0) { + console.warn('Invalid AutopilotConfig definition: missing or unrecognized max-bank value (expected a non-negative number)'); + maxBankAngle = AutopilotConfig.DEFAULT_MAX_BANK_ANGLE; + } + + return { maxBankAngle }; + } + + return { maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + } + + /** + * Parses LOC director options from a configuration document element. + * @param element A configuration document element. + * @returns The LOC director options defined by the configuration document element. + */ + private parseLocOptions(element: Element | null): AutopilotLocOptions { + if (element !== null) { + let maxBankAngle = Number(element.getAttribute('max-bank') ?? undefined); + if (isNaN(maxBankAngle) || maxBankAngle < 0) { + console.warn('Invalid AutopilotConfig definition: missing or unrecognized max-bank value (expected a non-negative number)'); + maxBankAngle = AutopilotConfig.DEFAULT_MAX_BANK_ANGLE; + } + + return { maxBankAngle }; + } + + return { maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + } + + /** + * Parses HDG director options from a configuration document element. + * @param element A configuration document element. + * @returns The HDG director options defined by the configuration document element. + */ + private parseLNavOptions(element: Element | null): AutopilotLNavOptions { + if (element !== null) { + let maxBankAngle = Number(element.getAttribute('max-bank') ?? undefined); + if (isNaN(maxBankAngle) || maxBankAngle < 0) { + console.warn('Invalid AutopilotConfig definition: missing or unrecognized max-bank value (expected a non-negative number)'); + maxBankAngle = AutopilotConfig.DEFAULT_MAX_BANK_ANGLE; + } + + return { maxBankAngle }; + } + + return { maxBankAngle: AutopilotConfig.DEFAULT_MAX_BANK_ANGLE }; + } + + /** + * Parses Low Bank Mode options from a configuration document element. + * @param element A configuration document element. + * @returns The Low Bank Mode options defined by the configuration document element. + */ + private parseLowBankOptions(element: Element | null): AutopilotLowBankOptions { + if (element !== null) { + let maxBankAngle = Number(element.getAttribute('max-bank') ?? undefined); + if (isNaN(maxBankAngle) || maxBankAngle < 0) { + console.warn('Invalid AutopilotConfig definition: missing or unrecognized max-bank value (expected a non-negative number)'); + maxBankAngle = AutopilotConfig.DEFAULT_MAX_BANK_ANGLE; + } + + return { maxBankAngle }; + } + + return { maxBankAngle: AutopilotConfig.DEFAULT_LOW_BANK_ANGLE }; + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/Autopilot/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/Autopilot/index.ts new file mode 100644 index 000000000..ab571a279 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/Autopilot/index.ts @@ -0,0 +1 @@ +export * from './AutopilotConfig'; \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/G1000AirframeOptionsManager.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/G1000AirframeOptionsManager.ts index 181df2b95..0af022a58 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/G1000AirframeOptionsManager.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/G1000AirframeOptionsManager.ts @@ -2,10 +2,18 @@ import { Annunciation, EventBus, Warning, XMLAnnunciationFactory, XMLExtendedGaugeConfig, XMLFunction, XMLGaugeConfigFactory, XMLWarningFactory } from '@microsoft/msfs-sdk'; +import { DefaultConfigFactory } from '../Config/DefaultConfigFactory'; +import { VSpeedGroup } from '../VSpeed/VSpeed'; +import { VSpeedGroupConfig } from '../VSpeed/VSpeedGroupConfig'; +import { AirspeedIndicatorConfig } from './AirspeedIndicator/AirspeedIndicatorConfig'; +import { AutopilotConfig } from './Autopilot/AutopilotConfig'; + /** * A manager for G1000 airframe options. */ export class G1000AirframeOptionsManager { + private readonly configFactory = new DefaultConfigFactory(); + private _hasRadioAltimeter = false; private _hasWeatherRadar = true; private _gaugeConfig: XMLExtendedGaugeConfig = { @@ -73,6 +81,36 @@ export class G1000AirframeOptionsManager { return this._warningConfig; } + private _autopilotConfig?: AutopilotConfig; + // eslint-disable-next-line jsdoc/require-returns + /** A config which defines options for the autopilot. */ + public get autopilotConfig(): AutopilotConfig { + if (!this._autopilotConfig) { + throw new Error('G1000AirframeOptionsManager: cannot access config before it has been parsed.'); + } + return this._autopilotConfig; + } + + private _vSpeedGroups?: ReadonlyMap; + // eslint-disable-next-line jsdoc/require-returns + /** Definitions for reference V-speeds. */ + public get vSpeedGroups(): ReadonlyMap { + if (!this._vSpeedGroups) { + throw new Error('G1000AirframeOptionsManager: cannot access config before it has been parsed.'); + } + return this._vSpeedGroups; + } + + private _airspeedIndicatorConfig?: AirspeedIndicatorConfig; + // eslint-disable-next-line jsdoc/require-returns + /** A config which defines options for the airspeed indicator. */ + public get airspeedIndicatorConfig(): AirspeedIndicatorConfig { + if (!this._airspeedIndicatorConfig) { + throw new Error('G1000AirframeOptionsManager: cannot access config before it has been parsed.'); + } + return this._airspeedIndicatorConfig; + } + /** * Parse the plane's EIS configuation. * @param document The configuration as an XML document. @@ -103,6 +141,8 @@ export class G1000AirframeOptionsManager { * Parse the panel.xml for airframe specific options. */ public parseConfig(): void { + const rootElement = this.instrument.xmlConfig.getElementsByTagName('PlaneHTMLConfig')[0]; + const instruments = this.instrument.xmlConfig.getElementsByTagName('Instrument'); for (const instrument of instruments) { const instrumentId = instrument.getElementsByTagName('Name'); @@ -123,5 +163,118 @@ export class G1000AirframeOptionsManager { this._gaugeConfig = this.parseGaugeConfig(this.instrument.xmlConfig); this._annunciationConfig = this.parseAnnunciationConfig(this.instrument.xmlConfig); this._warningConfig = this.parseWarningConfig(this.instrument.xmlConfig); + + this._autopilotConfig = this.parseAutopilotConfig(rootElement); + + this._vSpeedGroups = this.parseVSpeedGroups(rootElement); + + this._airspeedIndicatorConfig = this.parseAirspeedIndicatorConfig(rootElement); + } + + /** + * Parses an autopilot configuration object from a configuration document. If none can be found or parsed without + * error, then this method will return a default configuration object. + * @param config The root of the configuration document. + * @returns The autopilot configuration defined by the configuration document, or a default version if the document + * does not define a valid configuration. + */ + private parseAutopilotConfig(config: Element): AutopilotConfig { + try { + const autopilot = config.querySelector(':scope>Autopilot'); + if (autopilot !== null) { + return new AutopilotConfig(autopilot); + } + } catch (e) { + console.warn(e); + } + + return new AutopilotConfig(undefined); + } + + /** + * Parses reference V-speed definitions from a configuration document. If none can be found or parsed without error, + * then this method will return a default set of V-speed definitions. + * @param config The root of the configuration document. + * @returns Reference V-speed definitions defined by the configuration document, or a default version if the document + * does not define a valid configuration. + */ + private parseVSpeedGroups(config: Element): Map { + const element = config.querySelector(':scope>VSpeeds'); + + const map = new Map(); + + if (element === null) { + return this.getDefaultVSpeedGroups(); + } + + const children = Array.from(element.querySelectorAll(':scope>Group')); + const groups = children.map(child => { + try { + return new VSpeedGroupConfig(child).resolve(); + } catch (e) { + console.warn(e); + return null; + } + }); + + // Pick the first group of each type. + for (const group of groups) { + if (group === null) { + continue; + } + + if (!map.has(group.name)) { + map.set(group.name, group); + } + } + + return map; + } + + /** + * Gets a set of default reference V-speed definitions. The set contains definitions for the following V-speeds (in + * order): + * + * 1. V-speed name: `glide`, label: `GLIDE` + * 2. V-speed name: `r`, label: `Vr` + * 3. V-speed name: `x`, label: `Vx` + * 4. V-speed name: `y`, label: `Vy` + * + * The default values for the V-speeds are derived from the corresponding entries in the aircraft configuration + * files. + * @returns An array containing a set of default reference V-speed definitions. + */ + private getDefaultVSpeedGroups(): Map { + return new Map([ + ['', { + name: '', + vSpeedDefinitions: [ + { name: 'glide', defaultValue: Math.round(Simplane.getDesignSpeeds().BestGlide), label: 'GLIDE' }, + { name: 'r', defaultValue: Math.round(Simplane.getDesignSpeeds().Vr), label: 'Vr' }, + { name: 'x', defaultValue: Math.round(Simplane.getDesignSpeeds().Vx), label: 'Vx' }, + { name: 'y', defaultValue: Math.round(Simplane.getDesignSpeeds().Vy), label: 'Vy' } + ] + }] + ]); + } + + /** + * Parses an airspeed indicator configuration object from a configuration document. If none can be found or parsed + * without error, then this method will return a default configuration object. + * @param config The root of the configuration document. + * @returns The airspeed indicator configuration defined by the configuration document, or a default version if the + * document does not define a valid configuration. + */ + private parseAirspeedIndicatorConfig(config: Element): AirspeedIndicatorConfig { + try { + const airspeedIndicator = config.querySelector(':scope>AirspeedIndicator'); + if (airspeedIndicator !== null) { + return new AirspeedIndicatorConfig(airspeedIndicator, this.configFactory); + } + } catch (e) { + console.warn(e); + } + + return new AirspeedIndicatorConfig(undefined, this.configFactory); } } \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/G1000SettingSaveManager.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/G1000SettingSaveManager.ts index 3af4d25eb..614f5dd47 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/G1000SettingSaveManager.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/G1000SettingSaveManager.ts @@ -32,7 +32,7 @@ export class G1000SettingSaveManager extends UserSettingSaveManager { const settings = [ ...backlightSettingManager.getAllSettings(), ...pfdSettingManager.getAllSettings(), - ...mapSettingManager.getAllSettings(), + ...mapSettingManager.getAllSettings().filter(setting => setting.definition.name !== 'mapGroundNorthUpActive'), ...trafficSettingManager.getAllSettings().filter(setting => setting.definition.name !== 'trafficOperatingMode'), ...mfdNavDataBarSettingManager.getAllSettings(), ...unitsSettingManager.getAllSettings(), diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/index.ts index b3be30c8c..b2c2a8540 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/index.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Profiles/index.ts @@ -1,2 +1,5 @@ +export * from './AirspeedIndicator'; +export * from './Autopilot'; + export * from './G1000AirframeOptionsManager'; export * from './G1000SettingSaveManager'; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/StartupLogo.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/StartupLogo.tsx index 09a9a25b1..6071d29e5 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/StartupLogo.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/StartupLogo.tsx @@ -66,7 +66,7 @@ export class StartupLogo extends DisplayComponent { * @param evt The event that arrived. */ private handleHEvent = (evt: string): void => { - if (evt === `${this.props.eventPrefix}_ENT_Push` || evt === `${this.props.eventPrefix}_SOFTKEYS_12`) { + if (evt === `${this.props.eventPrefix}_ENT_Push` || evt === `${this.props.eventPrefix}_SOFTKEYS_12` || evt === 'AS1000_CONTROL_PAD_ENT_Push') { this.props.onConfirmation && this.props.onConfirmation(); this.props.bus.off('hEvent', this.handleHEvent); } @@ -94,7 +94,7 @@ export class StartupLogo extends DisplayComponent { - System WT1.2.7 + System WT1.3.3

@@ -116,4 +116,4 @@ export class StartupLogo extends DisplayComponent {

); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Common/g1k_common.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Common/g1k_common.css index 3ca59e5d4..247886860 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Common/g1k_common.css +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Common/g1k_common.css @@ -15,8 +15,8 @@ html { } html body { - font-family: Roboto-Regular; - font-size: 12; + font-family: Roboto; + font-size: 12px; color: white; height: 100%; width: 100%; @@ -83,6 +83,13 @@ html body { font-weight: normal; font-style: normal; } */ +@font-face { + font-family: "DejaVuSans-SemiBold"; + src: url("/Pages/VCockpit/Instruments/NavSystems/WTG1000/Assets/Fonts/DejaVuSans-SemiBold.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + #Mainframe { --refWidth: 1024; --refHeight: 768; @@ -124,6 +131,10 @@ html body { font-size: 20px; } +.size24 { + font-size: 24px; +} + .roboto-font { font-family: Roboto; } @@ -162,10 +173,6 @@ html body { .failed-box { display: none; -} - -.failed-instr .failed-box { - display: block; position: absolute; top: 0; left: 0; @@ -177,4 +184,8 @@ html body { --fail-fg-color: rgba(244, 0, 0, 1); background: linear-gradient(to top right, var(--fail-bg-color) calc(50% - 1.4px), var(--fail-fg-color), var(--fail-bg-color) calc(50% + 1.4px)), linear-gradient(to top left, var(--fail-bg-color) calc(50% - 1.4px), var(--fail-fg-color), var(--fail-bg-color) calc(50% + 1.4px)) !important; +} + +.failed-instr .failed-box { + display: block; } \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Controllers/ControlpadInputController.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Controllers/ControlpadInputController.ts new file mode 100644 index 000000000..01dd6bfc0 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Controllers/ControlpadInputController.ts @@ -0,0 +1,434 @@ +import { ConsumerSubject, DebounceTimer, EventBus, Radio, RadioEvents, RadioType, SimVarValueType, Subject } from '@microsoft/msfs-sdk'; + +import { NavComRadio } from '../../NavCom'; +import { FmsHEvent } from '../FmsHEvent'; +import { XpdrInputController } from './XpdrInputController'; + +/** + * Event published by the MFD view service to inhibit generic controlpad use. + */ +export interface MFDViewServiceEvents { + /** True, if the the generic handling of controlpad input (frequency, xpdr) shall be inhibited. + * The condition, which triggers true, is MFD's activeViewKey = 'NavMapPage', for all other active views th event is false. + */ + inhibitGenericControlpadUse: boolean; +} + +export enum ControlpadTargetInstrument { + MFD = 0, + PFD = 1 +} + +export enum GenericControlpadHandlingStates { + comInputArmed, // Default state, entering a '1' is directly considered as the first digit of a com frequency input + navInputArmed, // Enabled by the NAV key. After 10s without input -> fallback to default state + xpdrInputArmed, // Enabled by the XPDR key. After 10s without input -> fallback to default state + comInputStarted, // Entering a com frequency has been started. After 10s without input -> fallback to default state + navInputStarted, // Entering a nav frequency has been started. After 10s without input -> fallback to default state + crsInputArmed, // Not for digit entering, but after 10s without input -> fallback to default state + genericHandlingInhibited, // Whenever the view type on the MFD is not navmap. +} + +/** Simvar which indicates the control pad target instrument. */ +export enum ControlPadSimVars { + ControlPadTargetView = 'L:WT1000_ControlPad_Targetview', // 0: MFD, 1: PFD +} + +/** + * Controller that handles control pad input for the view service. + */ +export class ControlpadInputController { + + static readonly controlPadEventMap: Map = new Map([ + // For the controlpad keyboard input: + ['AS1000_CONTROL_PAD_A', FmsHEvent.A], + ['AS1000_CONTROL_PAD_B', FmsHEvent.B], + ['AS1000_CONTROL_PAD_C', FmsHEvent.C], + ['AS1000_CONTROL_PAD_D', FmsHEvent.D], + ['AS1000_CONTROL_PAD_E', FmsHEvent.E], + ['AS1000_CONTROL_PAD_F', FmsHEvent.F], + ['AS1000_CONTROL_PAD_G', FmsHEvent.G], + ['AS1000_CONTROL_PAD_H', FmsHEvent.H], + ['AS1000_CONTROL_PAD_I', FmsHEvent.I], + ['AS1000_CONTROL_PAD_J', FmsHEvent.J], + ['AS1000_CONTROL_PAD_K', FmsHEvent.K], + ['AS1000_CONTROL_PAD_L', FmsHEvent.L], + ['AS1000_CONTROL_PAD_M', FmsHEvent.M], + ['AS1000_CONTROL_PAD_N', FmsHEvent.N], + ['AS1000_CONTROL_PAD_O', FmsHEvent.O], + ['AS1000_CONTROL_PAD_P', FmsHEvent.P], + ['AS1000_CONTROL_PAD_Q', FmsHEvent.Q], + ['AS1000_CONTROL_PAD_R', FmsHEvent.R], + ['AS1000_CONTROL_PAD_S', FmsHEvent.S], + ['AS1000_CONTROL_PAD_T', FmsHEvent.T], + ['AS1000_CONTROL_PAD_U', FmsHEvent.U], + ['AS1000_CONTROL_PAD_V', FmsHEvent.V], + ['AS1000_CONTROL_PAD_W', FmsHEvent.W], + ['AS1000_CONTROL_PAD_X', FmsHEvent.X], + ['AS1000_CONTROL_PAD_Y', FmsHEvent.Y], + ['AS1000_CONTROL_PAD_Z', FmsHEvent.Z], + ['AS1000_CONTROL_PAD_SPC', FmsHEvent.SPC], + ['AS1000_CONTROL_PAD_0', FmsHEvent.D0], + ['AS1000_CONTROL_PAD_1', FmsHEvent.D1], + ['AS1000_CONTROL_PAD_2', FmsHEvent.D2], + ['AS1000_CONTROL_PAD_3', FmsHEvent.D3], + ['AS1000_CONTROL_PAD_4', FmsHEvent.D4], + ['AS1000_CONTROL_PAD_5', FmsHEvent.D5], + ['AS1000_CONTROL_PAD_6', FmsHEvent.D6], + ['AS1000_CONTROL_PAD_7', FmsHEvent.D7], + ['AS1000_CONTROL_PAD_8', FmsHEvent.D8], + ['AS1000_CONTROL_PAD_9', FmsHEvent.D9], + ['AS1000_CONTROL_PAD_Dot', FmsHEvent.Dot], + ['AS1000_CONTROL_PAD_BKSP', FmsHEvent.BKSP], + ['AS1000_CONTROL_PAD_PlusMinus', FmsHEvent.PlusMinus], + + // These common events can also be received from the control pad: + ['AS1000_CONTROL_PAD_FMS_Upper_INC', FmsHEvent.UPPER_INC], + ['AS1000_CONTROL_PAD_FMS_Upper_DEC', FmsHEvent.UPPER_DEC], + ['AS1000_CONTROL_PAD_FMS_Lower_INC', FmsHEvent.LOWER_INC], + ['AS1000_CONTROL_PAD_FMS_Lower_DEC', FmsHEvent.LOWER_DEC], + ['AS1000_CONTROL_PAD_MENU_Push', FmsHEvent.MENU], + ['AS1000_CONTROL_PAD_CLR', FmsHEvent.CLR], + ['AS1000_CONTROL_PAD_ENT_Push', FmsHEvent.ENT], + ['AS1000_CONTROL_PAD_FMS_Upper_PUSH', FmsHEvent.UPPER_PUSH], + ['AS1000_CONTROL_PAD_DIRECTTO', FmsHEvent.DIRECTTO], + ['AS1000_CONTROL_PAD_FPL_Push', FmsHEvent.FPL], + ['AS1000_CONTROL_PAD_PROC_Push', FmsHEvent.PROC], + ['AS1000_CONTROL_PAD_RANGE_INC', FmsHEvent.RANGE_INC], + ['AS1000_CONTROL_PAD_RANGE_DEC', FmsHEvent.RANGE_DEC], + ['AS1000_CONTROL_PAD_COM', FmsHEvent.COM], + ['AS1000_CONTROL_PAD_NAV', FmsHEvent.NAV], + ['AS1000_CONTROL_PAD_XPDR', FmsHEvent.XPDR], + ['AS1000_CONTROL_PAD_CRS', FmsHEvent.CRS], + ['AS1000_CONTROL_PAD_Home', FmsHEvent.HOME], + ]); + + protected readonly controlPadAcceptingEvents: string[] = ['AS1000_CONTROL_PAD_ENT_Push', 'AS1000_PFD_ENT_Push', 'AS1000_MFD_ENT_Push']; + + private readonly sub = this.bus.getSubscriber(); + private readonly inhibitGenericControlpadUseConsumer = ConsumerSubject.create(this.sub.on('inhibitGenericControlpadUse'), false); + private readonly stateFallbackTimer = new DebounceTimer(); + private controlpadState = Subject.create(GenericControlpadHandlingStates.comInputArmed); + + private readonly xpdrHandler = new XpdrInputController(this.bus, this.targetDisplay === ControlpadTargetInstrument.PFD); + + private radio1: Radio = ({ + index: 1, + activeFrequency: 0, + standbyFrequency: 0, + ident: null, + signal: 0, + radioType: (this.targetDisplay === ControlpadTargetInstrument.PFD) ? RadioType.Com : RadioType.Nav, + selected: true + }); + private radio2: Radio = ({ + index: 2, + activeFrequency: 0, + standbyFrequency: 0, + ident: null, + signal: 0, + radioType: (this.targetDisplay === ControlpadTargetInstrument.PFD) ? RadioType.Com : RadioType.Nav, + selected: false + }); + // private selectedRadio: Radio = this.radio1; + + private comRadio: NavComRadio | undefined; + private navRadio: NavComRadio | undefined; + + /** + * Constructs the controller. Each PFD and MFD have seperate instances of this controllers. The PFD controller maintains + * the COM frequencies and the MFD controller the NAV frequencies. + * @param bus The event bus. + * @param targetDisplay Enum that indicates for which instrument (PFD or MFD) this handler is running. + */ + constructor( + private readonly bus: EventBus, + private readonly targetDisplay: ControlpadTargetInstrument) { + this.sub.on('set_radio_state').handle((state) => { + if ((targetDisplay === ControlpadTargetInstrument.PFD) && (state.radioType === RadioType.Com)) { + if (state.index === 1) { + this.radio1 = state; + } else if (state.index === 2) { + this.radio2 = state; + } + } else if ((targetDisplay === ControlpadTargetInstrument.MFD) && (state.radioType === RadioType.Nav)) { + if (state.index === 1) { + this.radio1 = state; + } else if (state.index === 2) { + this.radio2 = state; + } + } + }, false); + + // Block controlpad use if the MFD is showing any other view than the nav map: + this.inhibitGenericControlpadUseConsumer.sub((genericUseInhibited) => { + if (genericUseInhibited) { + // Clear any running fallback timer and set the inhibit state: + this.stateFallbackTimer.clear(); + this.controlpadState.set(GenericControlpadHandlingStates.genericHandlingInhibited); + SimVar.SetSimVarValue('L:WT1000_ControlPad_ModeInput_Inhibited', SimVarValueType.Bool, true); + } else { + // After an inhibit phase, we always return to default state: + this.controlpadState.set(GenericControlpadHandlingStates.comInputArmed); + SimVar.SetSimVarValue('L:WT1000_ControlPad_ModeInput_Inhibited', SimVarValueType.Bool, false); + SimVar.SetSimVarValue('L:WT1000_ControlPad_Mode', SimVarValueType.Enum, 0); + } + }, true); + + this.controlpadState.sub((state) => { + this.armRadio(state); + }, true); + } + + /** + * Setter for the radio refs. + * @param comRadio com radio ref + * @param navRadio nav radio ref + */ + public setFrequencyElementRefs(comRadio: NavComRadio, navRadio: NavComRadio): void { + this.comRadio = comRadio; + this.navRadio = navRadio; + } + + /** + * This abstract method returns true, if the current instrument shall handle control pad events: + * @returns translated event as string + */ + private isControlpadTargetInstrument(): boolean { + const controlPadTarget = SimVar.GetSimVarValue(ControlPadSimVars.ControlPadTargetView, 'number'); + if (controlPadTarget !== undefined) { + // If the simvar is telling the target instrument, use it as discriminator: + return controlPadTarget === this.targetDisplay; + } else { + // we return true only for the MFD if the simvar does not exist: + return this.targetDisplay === ControlpadTargetInstrument.MFD; + } + } + + /** + * Handler for all control pad input. + * @param hEvent received hEvent. + * @returns true if the event is handled + */ + public handleControlPadEventInput(hEvent: string): boolean { + let isHandled = false; + + // We only continue, if the event is coming from controlpad: + if (ControlpadInputController.controlPadEventMap.has(hEvent)) { + // For the frequency and transponder input, we need a state event machine here: + switch (this.controlpadState.get()) { + case GenericControlpadHandlingStates.comInputArmed: + isHandled = this.handleComInputArmedState(hEvent); + break; + + case GenericControlpadHandlingStates.comInputStarted: + isHandled = this.handleComInputStartedState(hEvent); + this.scheduleDefaultStateFallback(); + break; + + case GenericControlpadHandlingStates.navInputArmed: + isHandled = this.handleNavInputArmedState(hEvent); + this.scheduleDefaultStateFallback(); + break; + + case GenericControlpadHandlingStates.navInputStarted: + isHandled = this.handleNavInputStartedState(hEvent); + this.scheduleDefaultStateFallback(); + break; + + case GenericControlpadHandlingStates.xpdrInputArmed: + // The XPDR is handled entirely in armed mode: + isHandled = this.xpdrHandler.handleXpdrEntry(hEvent); + if (isHandled) { + this.scheduleDefaultStateFallback(); + } else { + // Any unexpected event that isn't changing to another mode causes the return to default mode com input armed: + if (!['AS1000_CONTROL_PAD_NAV', 'AS1000_CONTROL_PAD_CRS'].includes(hEvent)) { + this.controlpadState.set(GenericControlpadHandlingStates.comInputArmed); + SimVar.SetSimVarValue('L:WT1000_ControlPad_Mode', SimVarValueType.Enum, 0); + } + } + break; + + case GenericControlpadHandlingStates.crsInputArmed: + if (['AS1000_PFD_CRS_INC', 'AS1000_PFD_CRS_DEC'].includes(hEvent)) { + this.scheduleDefaultStateFallback(); + } + break; + } + + // If the event is not yet handled, we run the generic state determination: + if (!isHandled) { + isHandled = this.genericNewStateDetermination(hEvent); + } + + // If the event is still not yet handled, we consider the event as handled, if the current instrument + // is not target for control pad input. This will e.g. prevent the PFD to receive control pad events, if + // the MFD is target instrument: + if (!isHandled) { + isHandled = this.isControlpadTargetInstrument() === false; + } + + } + return isHandled; + } + + /** + * Check for state changing events, which are state agnostic + * @param hEvent H event as string + * @returns if event was handled + */ + private genericNewStateDetermination(hEvent: string): boolean { + let isHandled = false; + // Don't allow any other mode activation while generic handling is inhibited: + if (this.controlpadState.get() !== GenericControlpadHandlingStates.genericHandlingInhibited) { + switch (hEvent) { + case 'AS1000_CONTROL_PAD_COM': + this.controlpadState.set(GenericControlpadHandlingStates.comInputArmed); + this.stateFallbackTimer.clear(); + isHandled = true; + break; + case 'AS1000_CONTROL_PAD_NAV': + this.controlpadState.set(GenericControlpadHandlingStates.navInputArmed); + this.scheduleDefaultStateFallback(); + isHandled = true; + break; + case 'AS1000_CONTROL_PAD_XPDR': + this.controlpadState.set(GenericControlpadHandlingStates.xpdrInputArmed); + this.scheduleDefaultStateFallback(); + this.xpdrHandler.startXpdrEntry(); + isHandled = true; + break; + case 'AS1000_CONTROL_PAD_CRS': + this.controlpadState.set(GenericControlpadHandlingStates.crsInputArmed); + if (this.targetDisplay === ControlpadTargetInstrument.PFD) { + this.scheduleDefaultStateFallback(); + isHandled = true; + } + break; + } + } + return isHandled; + } + + /** + * Handle comInputArmed state. + * + * @param hEvent H event as string + * @returns if event was handled + */ + private handleComInputArmedState(hEvent: string): boolean { + let isHandled = false; + // Check for the event(s) that trigger the comInputStarted state: + if ((this.targetDisplay === ControlpadTargetInstrument.PFD) && (hEvent === 'AS1000_CONTROL_PAD_1')) { + // As the first digit of a COM frequency always is a '1', checking for that is enough! + if (this.comRadio?.onInteractionEvent(FmsHEvent.D1)) { + isHandled = true; + } + } + if (isHandled) { this.controlpadState.set(GenericControlpadHandlingStates.comInputStarted); } + return isHandled; + } + + /** + * Handle comInputStarted state. + * + * @param hEvent H event as string + * @returns if event was handled + */ + private handleComInputStartedState(hEvent: string): boolean { + let isHandled = false; + const evt = ControlpadInputController.controlPadEventMap.get(hEvent); + if (evt !== undefined) { + isHandled = this.comRadio?.onInteractionEvent(evt) ?? false; + if (isHandled === false) { + // If event was not handled, fall back to default mode: + this.controlpadState.set(GenericControlpadHandlingStates.comInputArmed); + } + } + return isHandled; + } + + /** + * Handle navInputArmed state. + * + * @param hEvent H event as string + * @returns if event was handled + */ + private handleNavInputArmedState(hEvent: string): boolean { + let isHandled = false; + // Check for the event(s) that trigger the navInputStarted state: + if ((this.targetDisplay === ControlpadTargetInstrument.MFD) && (hEvent === 'AS1000_CONTROL_PAD_1')) { + // As the first digit of a NAV frequency always is a '1', checking for that is enough! + if (this.navRadio !== undefined) { + if (this.navRadio.onInteractionEvent(FmsHEvent.D1)) { + isHandled = true; + } + } + } + if (isHandled) { this.controlpadState.set(GenericControlpadHandlingStates.navInputStarted); } + return isHandled; + } + + /** + * Handle navInputStarted state. + * + * @param hEvent H event as string + * @returns if event was handled + */ + private handleNavInputStartedState(hEvent: string): boolean { + let isHandled = false; + const evt = ControlpadInputController.controlPadEventMap.get(hEvent); + if (evt !== undefined) { + isHandled = this.navRadio?.onInteractionEvent(evt) ?? false; + if (isHandled === false) { + // If event was not handled, fall back to default mode: + this.controlpadState.set(GenericControlpadHandlingStates.navInputArmed); + } + } + return isHandled; + } + + /** + * This method schedules a 10 second period, after which a fallback to the default generic handling state occurs (comInputArmed). + */ + private scheduleDefaultStateFallback(): void { + this.stateFallbackTimer.schedule(() => { + // Clean up where ever we stand: + switch (this.controlpadState.get()) { + case GenericControlpadHandlingStates.comInputStarted: + this.comRadio?.onInteractionEvent(FmsHEvent.CLR); + break; + case GenericControlpadHandlingStates.navInputStarted: + this.navRadio?.onInteractionEvent(FmsHEvent.CLR); + break; + case GenericControlpadHandlingStates.xpdrInputArmed: + this.xpdrHandler.handleXpdrEntry('AS1000_CONTROL_PAD_CLR'); + break; + } + this.controlpadState.set(GenericControlpadHandlingStates.comInputArmed); + SimVar.SetSimVarValue('L:WT1000_ControlPad_Mode', SimVarValueType.Enum, 0); + }, 10000); + } + + /** Handle changes in control pad state and arm the appropriate radio box. + * @param controlpadState the state of the control pad + */ + private armRadio(controlpadState: GenericControlpadHandlingStates): void { + switch (controlpadState) { + case GenericControlpadHandlingStates.comInputArmed: + case GenericControlpadHandlingStates.comInputStarted: + if (this.targetDisplay === ControlpadTargetInstrument.PFD) { this.comRadio?.setArmed(true); } + if (this.targetDisplay === ControlpadTargetInstrument.MFD) { this.navRadio?.setArmed(false); } + break; + case GenericControlpadHandlingStates.navInputArmed: + case GenericControlpadHandlingStates.navInputStarted: + if (this.targetDisplay === ControlpadTargetInstrument.PFD) { this.comRadio?.setArmed(false); } + if (this.targetDisplay === ControlpadTargetInstrument.MFD) { this.navRadio?.setArmed(true); } + break; + default: + if (this.targetDisplay === ControlpadTargetInstrument.PFD) { this.comRadio?.setArmed(false); } + if (this.targetDisplay === ControlpadTargetInstrument.MFD) { this.navRadio?.setArmed(false); } + } + } +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Controllers/XpdrInputController.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Controllers/XpdrInputController.ts new file mode 100644 index 000000000..79de868a2 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Controllers/XpdrInputController.ts @@ -0,0 +1,81 @@ +import { EventBus } from '@microsoft/msfs-sdk'; + +import { G1000ControlEvents, G1000ControlPublisher } from '../../G1000Events'; + +/** + * Controller that maps control pad input to the xpdr bus events. + */ +export class XpdrInputController { + private readonly controlPadXpdrInputMap: Map = new Map([ + ['AS1000_CONTROL_PAD_0', 0], + ['AS1000_CONTROL_PAD_1', 1], + ['AS1000_CONTROL_PAD_2', 2], + ['AS1000_CONTROL_PAD_3', 3], + ['AS1000_CONTROL_PAD_4', 4], + ['AS1000_CONTROL_PAD_5', 5], + ['AS1000_CONTROL_PAD_6', 6], + ['AS1000_CONTROL_PAD_7', 7], + ['AS1000_CONTROL_PAD_8', 8], + ['AS1000_CONTROL_PAD_9', 9], + ]); + private readonly validXpdrDigits = [0, 1, 2, 3, 4, 5, 6, 7]; + + private digitPosition = 0; + private readonly g1000ControlPublisher = new G1000ControlPublisher(this.bus); + + /** + * Constructs the controller. + * @param bus The event bus. + * @param isPfd true if the controller is for the PFD, false if for the MFD + */ + constructor(private readonly bus: EventBus, private readonly isPfd: boolean) { + this.g1000ControlPublisher.startPublish(); + } + /** + * Enters XPDR Entry mode + */ + public startXpdrEntry(): void { + this.digitPosition = 0; + this.publishEvent('xpdr_code_push', true); + } + + /** + * Handles the XPDR input event + * @param hEvent received hEvent + * @returns true if handled, false if the not + */ + public handleXpdrEntry(hEvent: string): boolean { + let isHandled = true; + const digit = this.controlPadXpdrInputMap.get(hEvent); + if (digit !== undefined) { + if (this.validXpdrDigits.includes(digit)) { + // We received a valid input for xpdr input: + this.publishEvent('xpdr_code_digit', digit); + this.digitPosition++; + if (this.digitPosition > 3) { + this.publishEvent('xpdr_code_push', false); + isHandled = false; + } + } + } else { + // we ignore XPDR button press when handling XPDR input + if (hEvent !== 'AS1000_CONTROL_PAD_XPDR') { + this.digitPosition = 0; + this.publishEvent('xpdr_code_push', false); + isHandled = false; + } + } + return isHandled; + } + + /** + * Publishes the event to the bus if the controller is for the PFD. + * @param event the event to publish + * @param value the value to publish + */ + private publishEvent(event: keyof G1000ControlEvents, value: number | boolean): void { + if (this.isPfd) { + this.g1000ControlPublisher.publishEvent(event, value); + } + } +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/DirectTo/DirectTo.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/DirectTo/DirectTo.tsx index 9dcaf46b0..9aa7cb31b 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/DirectTo/DirectTo.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/DirectTo/DirectTo.tsx @@ -10,7 +10,7 @@ import { NumberInput } from '../UIControls/NumberInput'; import { WaypointInput } from '../UIControls/WaypointInput'; import { DigitInput } from '../UiControls2/DigitInput'; import { G1000UiControlWrapper } from '../UiControls2/G1000UiControlWrapper'; -import { GenericNumberInput } from '../UiControls2/GenericNumberInput'; +import { CourseNumberInput } from '../UiControls2/CourseNumberInput'; import { UiView, UiViewProps } from '../UiView'; import { DirectToController } from './DirectToController'; import { DirectToStore } from './DirectToStore'; @@ -201,7 +201,7 @@ export abstract class DirectTo extends UiView - { digitValues[0].set(Math.floor(value / 10) * 10); @@ -216,7 +216,7 @@ export abstract class DirectTo extends UiView ones === 0 ? 37 : 36)} increment={1} scale={10} formatter={(value): string => value.toFixed().padStart(2, '0')} wrap /> tens === 360 ? 1 : 10)} increment={1} scale={1} wrap /> ° - + ); } diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/FmsHEvent.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/FmsHEvent.ts index 963434123..e8e7acbbd 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/FmsHEvent.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/FmsHEvent.ts @@ -17,5 +17,50 @@ export enum FmsHEvent { JOYSTICK_UP = 'JoystickUp', JOYSTICK_RIGHT = 'JoystickRight', JOYSTICK_DOWN = 'JoystickDown', - CLR_LONG = 'ClrLong' -} \ No newline at end of file + CLR_LONG = 'ClrLong', + A = 'A', + B = 'B', + C = 'C', + D = 'D', + E = 'E', + F = 'F', + G = 'G', + H = 'H', + I = 'I', + J = 'J', + K = 'K', + L = 'L', + M = 'M', + N = 'N', + O = 'O', + P = 'P', + Q = 'Q', + R = 'R', + S = 'S', + T = 'T', + U = 'U', + V = 'V', + W = 'W', + X = 'X', + Y = 'Y', + Z = 'Z', + SPC = 'SPC', + D0 = '0', + D1 = '1', + D2 = '2', + D3 = '3', + D4 = '4', + D5 = '5', + D6 = '6', + D7 = '7', + D8 = '8', + D9 = '9', + Dot = 'Dot', + BKSP = 'BKSP', + PlusMinus = 'PlusMinus', + HOME = 'Home', + COM = 'Com', + NAV = 'Nav', + XPDR = 'Xpdr', + CRS = 'Crs', +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/G1000UiControl.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/G1000UiControl.tsx index 1b049ef62..121647d2b 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/G1000UiControl.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/G1000UiControl.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { FSComponent, HardwareControlListProps, HardwareUiControl, HardwareUiControlList, HardwareUiControlProps, UiControlEventHandler, UiControlEventHandlers, UiControlPropEventHandlers, VNode @@ -216,214 +217,1048 @@ export class G1000UiControl

public onJoystickDown(source: G1000UiControl): boolean { return this.props.onJoystickDown ? this.props.onJoystickDown(source) : false; } + + /** + * Handles the A key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onA(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.A); + } + + /** + * Handles the B key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onB(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.B); + } + + /** + * Handles the C key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onC(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.C); + } + + /** + * Handles the D key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onD(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D); + } + + /** + * Handles the E key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onE(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.E); + } + + /** + * Handles the F key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onF(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.F); + } + + /** + * Handles the G key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onG(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.G); + } + + /** + * Handles the H key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onH(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.H); + } + + /** + * Handles the I key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onI(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.I); + } + + /** + * Handles the J key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onJ(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.J); + } + + /** + * Handles the K key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onK(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.K); + } + + /** + * Handles the L key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onL(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.L); + } + + /** + * Handles the M key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onM(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.M); + } + + /** + * Handles the N key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onN(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.N); + } + + /** + * Handles the O key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onO(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.O); + } + + /** + * Handles the P key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onP(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.P); + } + + /** + * Handles the Q key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onQ(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.Q); + } + + /** + * Handles the R key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onR(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.R); + } + + /** + * Handles the S key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onS(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.S); + } + + /** + * Handles the T key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onT(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.T); + } + + /** + * Handles the U key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onU(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.U); + } + + /** + * Handles the V key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onV(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.V); + } + + /** + * Handles the W key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onW(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.W); + } + + /** + * Handles the X key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onX(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.X); + } + + /** + * Handles the Y key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onY(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.Y); + } + + /** + * Handles the Z key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onZ(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.Z); + } + + /** + * Handles the key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onSPC(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.SPC); + } + + /** + * Handles the 0 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on0(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D0); + } + + /** + * Handles the 1 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on1(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D1); + } + + /** + * Handles the 2 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on2(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D2); + } + + /** + * Handles the 3 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on3(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D3); + } + + /** + * Handles the 4 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on4(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D4); + } + + /** + * Handles the 5 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on5(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D5); + } + + /** + * Handles the 6 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on6(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D6); + } + + /** + * Handles the 7 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on7(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D7); + } + + /** + * Handles the 8 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on8(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D8); + } + + /** + * Handles the 9 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on9(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D9); + } + + /** + * Handles the Dot key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onDot(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.Dot); + } + + /** + * Handles the BKSP key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onBKSP(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.BKSP); + } + + /** + * Handles the +/- key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onPlusMinus(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.PlusMinus); + } + + /** + * Handles the Home key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onHome(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.HOME); + } + + /** + * Handles the COM key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onCom(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.COM); + } + + /** + * Handles the NAV key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onNav(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.NAV); + } + + /** + * Handles the XPDR key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onXpdr(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.XPDR); + } + + /** + * Handles the CRS key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onCrs(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.CRS); + } + + /** + * Consolidates all key events and allows sub classes to override and implement specific keyboard input behaviors. + * @param source The source of the event. + * @param evt FmsHEvent of the keyboard event. + * @returns always false for the top level version of the method. + */ + public consolidateKeyboardHEvent(source: G1000UiControl, evt: FmsHEvent): boolean { + return false; + } + +} + +/** Properties on the GarminControlList component. */ +export interface GarminControlListProps extends UiControlPropEventHandlers, HardwareUiControlProps, HardwareControlListProps { } -/** Properties on the GarminControlList component. */ -export interface GarminControlListProps extends UiControlPropEventHandlers, HardwareUiControlProps, HardwareControlListProps { -} +/** + * A component that holds lists of G1000UiControls. + */ +export class G1000ControlList + extends HardwareUiControlList> + implements UiControlEventHandlers { + + /** @inheritdoc */ + public onInteractionEvent(evt: FmsHEvent): boolean { + switch (evt) { + case FmsHEvent.UPPER_INC: + if (this.props.innerKnobScroll) { + return this.scroll('forward'); + } + break; + case FmsHEvent.UPPER_DEC: + if (this.props.innerKnobScroll) { + return this.scroll('backward'); + } + break; + case FmsHEvent.LOWER_INC: + return this.scroll('forward'); + case FmsHEvent.LOWER_DEC: + return this.scroll('backward'); + } + + return this.triggerEvent(evt, this); + } + + /** + * Handles FMS upper knob increase events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onUpperKnobInc(source: G1000UiControl): boolean { + return this.props.onUpperKnobInc ? this.props.onUpperKnobInc(source) : false; + } + + /** + * Handles FMS upper knob decrease events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onUpperKnobDec(source: G1000UiControl): boolean { + return this.props.onUpperKnobDec ? this.props.onUpperKnobDec(source) : false; + } + + /** + * Handles FMS lower knob increase events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onLowerKnobInc(source: G1000UiControl): boolean { + return this.props.onLowerKnobInc ? this.props.onLowerKnobInc(source) : false; + } + + /** + * Handles FMS lower knob decrease events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onLowerKnobDec(source: G1000UiControl): boolean { + return this.props.onLowerKnobDec ? this.props.onLowerKnobDec(source) : false; + } + + /** + * Handles FMS upper knob push events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onUpperKnobPush(source: G1000UiControl): boolean { + return this.props.onUpperKnobPush ? this.props.onUpperKnobPush(source) : false; + } + + /** + * Handles MENU button press events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onMenu(source: G1000UiControl): boolean { + return this.props.onMenu ? this.props.onMenu(source) : false; + } + + /** + * Handles ENTER button press events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onEnter(source: G1000UiControl): boolean { + return this.props.onEnter ? this.props.onEnter(source) : false; + } + + /** + * Handles CLR button press events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onClr(source: G1000UiControl): boolean { + return this.props.onClr ? this.props.onClr(source) : false; + } + + /** + * Handles CLR button long press events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onClrLong(source: G1000UiControl): boolean { + return this.props.onClrLong ? this.props.onClrLong(source) : false; + } + + /** + * Handles DRCT button press events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onDirectTo(source: G1000UiControl): boolean { + return this.props.onDirectTo ? this.props.onDirectTo(source) : false; + } + + /** + * Handles FPL button press events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onFPL(source: G1000UiControl): boolean { + return this.props.onFPL ? this.props.onFPL(source) : false; + } + + /** + * Handles PROC button press events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onProc(source: G1000UiControl): boolean { + return this.props.onProc ? this.props.onProc(source) : false; + } + + /** + * Handles range joystick increase events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onRangeInc(source: G1000UiControl): boolean { + return this.props.onRangeInc ? this.props.onRangeInc(source) : false; + } + + /** + * Handles range joystick decrease events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onRangeDec(source: G1000UiControl): boolean { + return this.props.onRangeDec ? this.props.onRangeDec(source) : false; + } + + /** + * Handles range joystick push events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onJoystickPush(source: G1000UiControl): boolean { + return this.props.onJoystickPush ? this.props.onJoystickPush(source) : false; + } + + /** + * Handles range joystick left deflection events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onJoystickLeft(source: G1000UiControl): boolean { + return this.props.onJoystickLeft ? this.props.onJoystickLeft(source) : false; + } + + /** + * Handles range joystick up deflection events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onJoystickUp(source: G1000UiControl): boolean { + return this.props.onJoystickUp ? this.props.onJoystickUp(source) : false; + } + + /** + * Handles range joystick right deflection events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onJoystickRight(source: G1000UiControl): boolean { + return this.props.onJoystickRight ? this.props.onJoystickRight(source) : false; + } + + /** + * Handles range joystick down deflection events. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onJoystickDown(source: G1000UiControl): boolean { + return this.props.onJoystickDown ? this.props.onJoystickDown(source) : false; + } -/** - * A component that holds lists of G1000UiControls. - */ -export class G1000ControlList - extends HardwareUiControlList> - implements UiControlEventHandlers { + /** + * Handles the A key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onA(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.A); + } - /** @inheritdoc */ - public onInteractionEvent(evt: FmsHEvent): boolean { - switch (evt) { - case FmsHEvent.UPPER_INC: - if (this.props.innerKnobScroll) { - return this.scroll('forward'); - } - break; - case FmsHEvent.UPPER_DEC: - if (this.props.innerKnobScroll) { - return this.scroll('backward'); - } - break; - case FmsHEvent.LOWER_INC: - return this.scroll('forward'); - case FmsHEvent.LOWER_DEC: - return this.scroll('backward'); - } + /** + * Handles the B key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onB(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.B); + } - return this.triggerEvent(evt, this); + /** + * Handles the C key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onC(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.C); } /** - * Handles FMS upper knob increase events. + * Handles the D key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onUpperKnobInc(source: G1000UiControl): boolean { - return this.props.onUpperKnobInc ? this.props.onUpperKnobInc(source) : false; + public onD(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D); } /** - * Handles FMS upper knob decrease events. + * Handles the E key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onUpperKnobDec(source: G1000UiControl): boolean { - return this.props.onUpperKnobDec ? this.props.onUpperKnobDec(source) : false; + public onE(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.E); } /** - * Handles FMS lower knob increase events. + * Handles the F key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onLowerKnobInc(source: G1000UiControl): boolean { - return this.props.onLowerKnobInc ? this.props.onLowerKnobInc(source) : false; + public onF(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.F); } /** - * Handles FMS lower knob decrease events. + * Handles the G key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onLowerKnobDec(source: G1000UiControl): boolean { - return this.props.onLowerKnobDec ? this.props.onLowerKnobDec(source) : false; + public onG(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.G); } /** - * Handles FMS upper knob push events. + * Handles the H key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onUpperKnobPush(source: G1000UiControl): boolean { - return this.props.onUpperKnobPush ? this.props.onUpperKnobPush(source) : false; + public onH(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.H); } /** - * Handles MENU button press events. + * Handles the I key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onMenu(source: G1000UiControl): boolean { - return this.props.onMenu ? this.props.onMenu(source) : false; + public onI(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.I); } /** - * Handles ENTER button press events. + * Handles the J key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onEnter(source: G1000UiControl): boolean { - return this.props.onEnter ? this.props.onEnter(source) : false; + public onJ(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.J); } /** - * Handles CLR button press events. + * Handles the K key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onClr(source: G1000UiControl): boolean { - return this.props.onClr ? this.props.onClr(source) : false; + public onK(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.K); } /** - * Handles CLR button long press events. + * Handles the L key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onClrLong(source: G1000UiControl): boolean { - return this.props.onClrLong ? this.props.onClrLong(source) : false; + public onL(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.L); } /** - * Handles DRCT button press events. + * Handles the M key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onDirectTo(source: G1000UiControl): boolean { - return this.props.onDirectTo ? this.props.onDirectTo(source) : false; + public onM(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.M); } /** - * Handles FPL button press events. + * Handles the N key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onFPL(source: G1000UiControl): boolean { - return this.props.onFPL ? this.props.onFPL(source) : false; + public onN(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.N); } /** - * Handles PROC button press events. + * Handles the O key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onProc(source: G1000UiControl): boolean { - return this.props.onProc ? this.props.onProc(source) : false; + public onO(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.O); } /** - * Handles range joystick increase events. + * Handles the P key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onRangeInc(source: G1000UiControl): boolean { - return this.props.onRangeInc ? this.props.onRangeInc(source) : false; + public onP(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.P); } /** - * Handles range joystick decrease events. + * Handles the Q key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onRangeDec(source: G1000UiControl): boolean { - return this.props.onRangeDec ? this.props.onRangeDec(source) : false; + public onQ(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.Q); } /** - * Handles range joystick push events. + * Handles the R key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onJoystickPush(source: G1000UiControl): boolean { - return this.props.onJoystickPush ? this.props.onJoystickPush(source) : false; + public onR(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.R); } /** - * Handles range joystick left deflection events. + * Handles the S key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onJoystickLeft(source: G1000UiControl): boolean { - return this.props.onJoystickLeft ? this.props.onJoystickLeft(source) : false; + public onS(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.S); } /** - * Handles range joystick up deflection events. + * Handles the T key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onJoystickUp(source: G1000UiControl): boolean { - return this.props.onJoystickUp ? this.props.onJoystickUp(source) : false; + public onT(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.T); } /** - * Handles range joystick right deflection events. + * Handles the U key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onJoystickRight(source: G1000UiControl): boolean { - return this.props.onJoystickRight ? this.props.onJoystickRight(source) : false; + public onU(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.U); } /** - * Handles range joystick down deflection events. + * Handles the V key. * @param source The source of the event. * @returns Whether the event was handled. */ - public onJoystickDown(source: G1000UiControl): boolean { - return this.props.onJoystickDown ? this.props.onJoystickDown(source) : false; + public onV(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.V); + } + + /** + * Handles the W key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onW(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.W); + } + + /** + * Handles the X key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onX(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.X); + } + + /** + * Handles the Y key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onY(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.Y); + } + + /** + * Handles the Z key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onZ(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.Z); + } + + /** + * Handles the key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onSPC(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.SPC); + } + + /** + * Handles the 0 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on0(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D0); + } + + /** + * Handles the 1 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on1(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D1); + } + + /** + * Handles the 2 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on2(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D2); + } + + /** + * Handles the 3 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on3(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D3); + } + + /** + * Handles the 4 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on4(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D4); + } + + /** + * Handles the 5 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on5(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D5); + } + + /** + * Handles the 6 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on6(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D6); + } + + /** + * Handles the 7 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on7(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D7); + } + + /** + * Handles the 8 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on8(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D8); + } + + /** + * Handles the 9 key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public on9(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.D9); + } + + /** + * Handles the Dot key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onDot(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.Dot); + } + + /** + * Handles the BKSP key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onBKSP(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.BKSP); + } + + /** + * Handles the +/- key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onPlusMinus(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.PlusMinus); + } + + /** + * Handles the Home key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onHome(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.HOME); + } + + /** + * Handles the COM key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onCom(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.COM); + } + + /** + * Handles the NAV key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onNav(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.NAV); + } + + /** + * Handles the XPDR key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onXpdr(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.XPDR); + } + + /** + * Handles the CRS key. + * @param source The source of the event. + * @returns Whether the event was handled. + */ + public onCrs(source: G1000UiControl): boolean { + return this.consolidateKeyboardHEvent(source, FmsHEvent.CRS); + } + + /** + * Consolidates all key events and allows sub classes to override and implement specific keyboard input behaviors. + * @param source The source of the event. + * @param evt FmsHEvent of the keyboard event. + * @returns always false for the top level version of the method. + */ + public consolidateKeyboardHEvent(source: G1000UiControl, evt: FmsHEvent): boolean { + return false; } + + + /** @inheritdoc */ protected renderScrollbar(): VNode { return (); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/MFD/LeanMenu.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/MFD/LeanMenu.ts index 67b549be1..0e6104abd 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/MFD/LeanMenu.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/MFD/LeanMenu.ts @@ -11,7 +11,6 @@ import { SoftKeyMenu } from '../SoftKeyMenu'; export class LeanMenu extends SoftKeyMenu { private publisher: G1000ControlPublisher; private leanAssistActive = false; - private cylSlctActive = false; /** * Creates an instance of the MFD engine lean menu. diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/RootMenu.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/RootMenu.ts index 58ab3a57a..0580edd7c 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/RootMenu.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/RootMenu.ts @@ -1,11 +1,14 @@ -import { ComputedSubject, ConsumerSubject, ControlPublisher, EventBus, InstrumentEvents, LNavEvents, MappedSubject, NavEvents } from '@microsoft/msfs-sdk'; +import { + ComputedSubject, ConsumerSubject, ControlPublisher, DebounceTimer, EventBus, InstrumentEvents, LNavEvents, MappedSubject, NavEvents +} from '@microsoft/msfs-sdk'; import { GarminControlEvents, ObsSuspModes } from '@microsoft/msfs-garminsdk'; +import { G1000AlertsLevel, G1000CasEvents } from '../../../PFD/Components/FlightInstruments'; import { AlertMessageEvents } from '../../../PFD/Components/UI/Alerts/AlertsSubject'; import { G1000ControlPublisher } from '../../G1000Events'; -import { SoftKeyMenuSystem } from './SoftKeyMenuSystem'; import { SoftKeyMenu } from './SoftKeyMenu'; +import { SoftKeyMenuSystem } from './SoftKeyMenuSystem'; /** * The root PFD softkey menu. @@ -27,6 +30,8 @@ export class RootMenu extends SoftKeyMenu { }); private mfdPoweredOn = false; + private currentAlertsLevel = G1000AlertsLevel.None; + private readonly alertStylingDebounceTimer = new DebounceTimer(); /** * Creates an instance of the root PFD softkey menu. @@ -76,7 +81,7 @@ export class RootMenu extends SoftKeyMenu { g1000Publisher.publishEvent('pfd_alert_push', true); }, undefined, false); - const sub = bus.getSubscriber(); + const sub = bus.getSubscriber(); this.isLNavSuspended = ConsumerSubject.create(sub.on('lnav_is_suspended'), false); this.isObsActive = ConsumerSubject.create(sub.on('gps_obs_active'), false); @@ -134,6 +139,18 @@ export class RootMenu extends SoftKeyMenu { sub.on('alerts_available') .handle(available => this.getItem(11).highlighted.set(available)); + + sub.on('cas_unacknowledged_alerts_level').handle(v => { + this.currentAlertsLevel = v; + this.getItem(11).additionalClasses.clear(); + + //We have a debounce here so we can reset and re-add the CSS animations to restart + //their timing. This ensures that the Alerts button flashes in sync with any CAS + //alerts + if (!this.alertStylingDebounceTimer.isPending()) { + this.alertStylingDebounceTimer.schedule(this.updateAlertsStyle, 0); + } + }); } /** @@ -150,4 +167,30 @@ export class RootMenu extends SoftKeyMenu { }, 1000); } }; + + /** + * Updates the Alerts button style. + */ + private updateAlertsStyle = (): void => { + const item = this.getItem(11); + + switch (this.currentAlertsLevel) { + case G1000AlertsLevel.None: + item.label.set('Alerts'); + item.additionalClasses.clear(); + break; + case G1000AlertsLevel.Advisory: + item.label.set('Advisory'); + item.additionalClasses.set(['advisory']); + break; + case G1000AlertsLevel.Caution: + item.label.set('Caution'); + item.additionalClasses.set(['caution']); + break; + case G1000AlertsLevel.Warning: + item.label.set('Warning'); + item.additionalClasses.set(['warning']); + break; + } + }; } diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKey.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKey.css index 292935a09..a80d249ee 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKey.css +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKey.css @@ -12,7 +12,7 @@ --value-color: cyan; } -.softkey-tab.text-disabled{ +.softkey-tab.text-disabled { --value-color: rgb(18, 95, 95); } @@ -21,6 +21,84 @@ color: black; } +@keyframes warning-softkey-animation { + 0% { + background: linear-gradient(to bottom, #ef3524 40%, #911); + color: white; + } + + 66% { + background: linear-gradient(to bottom, #ef3524 40%, #911); + color: white; + } + + 66.1% { + background: unset; + color: white; + } + + 99.9% { + background: unset; + color: white; + } +} + +@keyframes caution-softkey-animation { + 0% { + background: linear-gradient(to bottom, #df0 40%, #890); + color: black; + } + + 66% { + background: linear-gradient(to bottom, #df0 40%, #890); + color: black; + } + + 66.1% { + background: unset; + color: white; + } + + 99.9% { + background: unset; + color: white; + } +} + +@keyframes advisory-softkey-animation { + 0% { + background: rgb(160, 160, 160); + color: black; + } + + 66% { + background: rgb(160, 160, 160); + color: black; + } + + 66.1% { + background: unset; + color: white; + } + + 99.9% { + background: unset; + color: white; + } +} + +.softkey-tab.warning { + animation: warning-softkey-animation 1s linear 0s infinite normal; +} + +.softkey-tab.caution { + animation: caution-softkey-animation 1s linear 0s infinite normal; +} + +.softkey-tab.advisory { + animation: advisory-softkey-animation 1s linear 0s infinite normal; +} + .softkey-tab.pressed { background: linear-gradient(to top, #091429 60%, #1b303f 80%, #36576e); } @@ -42,7 +120,7 @@ .softkey-tab-borders { border-width: 1px; - border-image: linear-gradient( to bottom, #3f3f3f 0%, #080808 50%) 1 100%; + border-image: linear-gradient(to bottom, #3f3f3f 0%, #080808 50%) 1 100%; position: absolute; left: -1px; width: calc(100% + 2px); diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKey.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKey.tsx index 9a52057a0..35bf8b7fc 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKey.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKey.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, DisplayComponent, FSComponent, NodeReference, VNode } from '@microsoft/msfs-sdk'; +import { ComponentProps, DisplayComponent, FSComponent, NodeReference, SetSubject, SubscribableSetEventType, VNode } from '@microsoft/msfs-sdk'; import { MenuItem, SoftKeyMenu } from './SoftKeyMenu'; @@ -22,6 +22,7 @@ export class SoftKey extends DisplayComponent { private readonly labelEl = new NodeReference(); private readonly indicatorEl = new NodeReference(); private readonly valueEl = new NodeReference(); + private readonly classList = SetSubject.create(['softkey-tab']); /** @inheritdoc */ onAfterRender(node: VNode): void { @@ -54,6 +55,9 @@ export class SoftKey extends DisplayComponent { this.setHighlighted(item.highlighted.get()); item.highlighted.sub(this.setHighlighted); + + item.additionalClasses.get().forEach(v => this.classList.add(v)); + item.additionalClasses.sub(this.setAdditionalClasses); } /** @@ -66,6 +70,8 @@ export class SoftKey extends DisplayComponent { item.pressed.off(this.setPressed); item.label.unsub(this.setLabel); item.highlighted.unsub(this.setHighlighted); + + item.additionalClasses.get().forEach(v => this.classList.delete(v)); } /** @@ -164,9 +170,23 @@ export class SoftKey extends DisplayComponent { */ private setHighlighted = (isHighlighted: boolean): void => { if (isHighlighted) { - this.rootEl.instance.classList.add('highlighted'); + this.classList.add('highlighted'); + } else { + this.classList.delete('highlighted'); + } + }; + + /** + * Sets additional classes on the soft key item. + * @param set The full current set of additional classes. + * @param type The type of event. + * @param key The key that was changed. + */ + private setAdditionalClasses = (set: ReadonlySet, type: SubscribableSetEventType, key: string): void => { + if (type === SubscribableSetEventType.Added) { + this.classList.add(key); } else { - this.rootEl.instance.classList.remove('highlighted'); + this.classList.delete(key); } }; @@ -176,7 +196,7 @@ export class SoftKey extends DisplayComponent { */ public render(): VNode { return ( -

+
diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKeyMenu.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKeyMenu.ts index 882444e58..069169ea4 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKeyMenu.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/SoftKeyMenu.ts @@ -1,4 +1,4 @@ -import { SubEvent, Subject } from '@microsoft/msfs-sdk'; +import { SetSubject, SubEvent, Subject } from '@microsoft/msfs-sdk'; import { SoftKeyMenuSystem } from './SoftKeyMenuSystem'; @@ -24,6 +24,9 @@ export interface MenuItem { /** Whether or not the menu item is highlighted. */ highlighted: Subject; + + /** Additional CSS classes to add to the menu item display. */ + additionalClasses: SetSubject; } /** @@ -58,7 +61,8 @@ export class SoftKeyMenu { value: Subject.create(value), pressed: new SubEvent(), disabled: Subject.create(handler === undefined || disabled), - highlighted: Subject.create(false) + highlighted: Subject.create(false), + additionalClasses: SetSubject.create() }; } @@ -116,6 +120,7 @@ export class SoftKeyMenu { disabled: Subject.create(true), pressed: new SubEvent(), value: Subject.create(undefined), - highlighted: Subject.create(false) + highlighted: Subject.create(false), + additionalClasses: SetSubject.create() }; } \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/XPDRCodeMenu.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/XPDRCodeMenu.ts index 643155aa0..69eae6182 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/XPDRCodeMenu.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/XPDRCodeMenu.ts @@ -8,7 +8,7 @@ import { SoftKeyMenu } from './SoftKeyMenu'; * The XPDR softkey menu. */ export class XPDRCodeMenu extends SoftKeyMenu { - + public xpdrCodeMenuActive = false; /** * Creates an instance of the transponder code menu. @@ -16,7 +16,7 @@ export class XPDRCodeMenu extends SoftKeyMenu { * @param bus is the event bus * @param g1000Publisher the G1000 control events publisher */ - constructor(menuSystem: SoftKeyMenuSystem, bus: EventBus, g1000Publisher: G1000ControlPublisher) { + constructor(menuSystem: SoftKeyMenuSystem, bus: EventBus, private readonly g1000Publisher: G1000ControlPublisher) { super(menuSystem); this.addItem(0, '0', () => { @@ -64,7 +64,11 @@ export class XPDRCodeMenu extends SoftKeyMenu { private onNewCodeFromSim(bus: EventBus, menuSystem: SoftKeyMenuSystem): void { const controlPublisher = bus.getSubscriber(); controlPublisher.on('publish_xpdr_code_1').handle(() => { - menuSystem.back(); + if (this.xpdrCodeMenuActive) { + this.xpdrCodeMenuActive = false; + this.g1000Publisher.publishEvent('xpdr_code_push', false); + menuSystem.back(); + } }); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/XPDRMenu.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/XPDRMenu.ts index deab332d7..efb4d1f01 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/XPDRMenu.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/Menus/XPDRMenu.ts @@ -3,6 +3,7 @@ import { ControlPublisher, EventBus, Subject, XPDRMode, XPDRSimVarEvents } from import { G1000ControlPublisher } from '../../G1000Events'; import { SoftKeyMenuSystem } from './SoftKeyMenuSystem'; import { SoftKeyMenu } from './SoftKeyMenu'; +import { XPDRCodeMenu } from './XPDRCodeMenu'; /** * The XPDR softkey menu. @@ -40,6 +41,8 @@ export class XPDRMenu extends SoftKeyMenu { controlPublisher.publishEvent('publish_xpdr_code_1', this.getVfrCode()); }, this.isVfr.get()); this.addItem(7, 'Code', () => { + const xpdrCodeMenu = this.menuSystem.getMenu('xpdr-code') as XPDRCodeMenu; + xpdrCodeMenu.xpdrCodeMenuActive = true; menuSystem.pushMenu('xpdr-code'); g1000Publisher.publishEvent('xpdr_code_push', true); }, false); @@ -97,4 +100,4 @@ export class XPDRMenu extends SoftKeyMenu { return 7000; } } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/ArrowToggle.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/ArrowToggle.tsx index 31d55e693..c162893a6 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/ArrowToggle.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/ArrowToggle.tsx @@ -1,4 +1,7 @@ -import { ArraySubject, ComponentProps, ComputedSubject, DisplayComponent, EventBus, FSComponent, Subject, SubscribableArrayEventType, VNode } from '@microsoft/msfs-sdk'; +import { + ArraySubject, ComponentProps, ComputedSubject, DisplayComponent, EventBus, FSComponent, MutableSubscribable, Subject, + SubscribableArrayEventType, VNode +} from '@microsoft/msfs-sdk'; import { UiControl, UiControlProps } from '../UiControl'; @@ -15,7 +18,7 @@ interface ArrowToggleComponentProps extends UiControlProps { onOptionSelected?(optionIndex: number): void; /** Data binding for the selected option */ - dataref?: Subject; + dataref?: MutableSubscribable; } /** diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/InputComponent.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/InputComponent.tsx index c7c77f72b..e2d14a18f 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/InputComponent.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/InputComponent.tsx @@ -1,13 +1,18 @@ -import { FSComponent, Subject, VNode } from '@microsoft/msfs-sdk'; +import { EventBus, FSComponent, Subject, VNode } from '@microsoft/msfs-sdk'; import { UiControl, UiControlProps } from '../UiControl'; import './InputComponent.css'; +import { ControlPadKeyOperations, ControlpadHEventHandler } from '../../Input/ControlpadHEventHandler'; +import { FmsHEvent } from '../FmsHEvent'; /** * @interface InputComponentProps */ interface InputComponentProps extends UiControlProps { + /** The event bus */ + bus: EventBus; + /** The max char length of this input field. */ maxLength: number; @@ -24,7 +29,7 @@ export class InputComponent extends UiControl { private readonly inputValueContainerRef = FSComponent.createRef(); private readonly selectedSpanRef = FSComponent.createRef(); - private keyboardInputHandler = this.handleTextboxInput.bind(this); + private readonly keyboardInputHandler = this.handleTextboxInput.bind(this); private readonly dataEntry = { text: '', @@ -39,9 +44,39 @@ export class InputComponent extends UiControl { private isKeyboardActive = false; private inputCharacterIndex = 0; private previousValue = ''; - private readonly inputId = this.genGuid(); + /** @inheritdoc */ + public onInteractionEvent(evt: FmsHEvent): boolean { + let isHandled = false; + const keyInputEvaluationResult = ControlpadHEventHandler.evaluateKeyboardInput(evt); + switch (keyInputEvaluationResult.KeyboardOperation) { + case ControlPadKeyOperations.InsertCharacter: + // Insert a character received from the controlPad keyboard. + if (!this.getIsActivated()) { + this.activate(); + } + if (keyInputEvaluationResult.ReceivedKey !== null) { + this.updateDataEntryElement(keyInputEvaluationResult.ReceivedKey); + this.dataEntry.highlightIndex++; + isHandled = true; + } + break; + + case ControlPadKeyOperations.ApplyBackSpace: + // Handle backspace keys received from the controlPad keyboard. + this.leftDeleteCharacter(); + isHandled = true; + break; + + default: + // For all other events we pass the event on: + isHandled = super.onInteractionEvent(evt); + break; + } + return isHandled; + } + /** * Method to set the initial text value when the component is made active. * @param value is a string containing the start text value @@ -139,6 +174,39 @@ export class InputComponent extends UiControl { this.dataEntry.afterSelected.set(afterText); } + /** + * Method to delete the character to the left of the selected index in the entry field (bkspc function). + * @param [emitEvent] A boolean indicating if a text changed event should be emitted. + */ + private leftDeleteCharacter(emitEvent = true): void { + if (this.dataEntry.highlightIndex !== undefined) { + const newSelectedIndex = Math.max(0, this.dataEntry.highlightIndex - 1); + + const text = this.dataEntry.text; + const blankFill = this.props.maxLength - this.dataEntry.highlightIndex; + + const beforeText = text.substr(0, newSelectedIndex); + const newSelectedChar = '_'; + const afterText = ''.padStart(blankFill, '_'); + + this.dataEntry.text = beforeText + newSelectedChar + afterText; + + if (emitEvent) { + this.props.onTextChanged(this.dataEntry.text.replace(/_/g, ' ').trim()); + } + + // We move the index one to the left and fetch the new selected char: + if (this.dataEntry.highlightIndex > 0) { + this.dataEntry.highlightIndex--; + } + + this.dataEntry.beforeSelected.set(beforeText); + this.dataEntry.selected.set(newSelectedChar); + this.dataEntry.afterSelected.set(afterText); + } + } + + /** * Handles the input from the hidden textbox */ @@ -219,6 +287,7 @@ export class InputComponent extends UiControl { * Method to handle on input blur */ private onInputBlur = (): void => { + ControlpadHEventHandler.clearPrefetchedCharacter(); this.textBoxRef.instance.disabled = true; this.textBoxRef.instance.value = ''; Coherent.off('SetInputTextFromOS', this.setValueFromOS); @@ -284,6 +353,7 @@ export class InputComponent extends UiControl { /** @inheritdoc */ public onEnter(): boolean { + ControlpadHEventHandler.clearPrefetchedCharacter(); if (this.getIsActivated()) { this.deactivate(); if (this.props.onEnter) { @@ -300,6 +370,7 @@ export class InputComponent extends UiControl { /** @inheritdoc */ public onClr(): boolean { + ControlpadHEventHandler.clearPrefetchedCharacter(); if (this.getIsActivated()) { this.setText(this.previousValue); this.deactivate(); @@ -324,8 +395,24 @@ export class InputComponent extends UiControl { this.textBoxRef.instance.onblur = this.onInputBlur; this.textBoxRef.instance.blur(); - // Make sure we deactivate ourselves if we lose focus. - this.focusSubject.sub((v, rv) => { !rv && this.isActivated && this.deactivate(); }); + this.focusSubject.sub((v, rv) => { + // Make sure we deactivate ourselves if we lose focus. + if (!rv && this.isActivated) { + this.deactivate(); + ControlpadHEventHandler.clearPrefetchedCharacter(); + } else if (rv === true) { + // In case the component has focus, check for the existences of a prefetched character: + const prefetchedCharacter = ControlpadHEventHandler.getPrefetchedCharacter(); + if (prefetchedCharacter !== undefined) { + this.dataEntry.highlightIndex = 0; + if (!this.getIsActivated()) { + this.activate(); + } + this.updateDataEntryElement(prefetchedCharacter, true); + this.dataEntry.highlightIndex++; + } + } + }); } /** @inheritdoc */ diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/NumberInput.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/NumberInput.tsx index 023b7095f..8b0c2d7ab 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/NumberInput.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/NumberInput.tsx @@ -1,4 +1,4 @@ -import { FSComponent, Subject, VNode } from '@microsoft/msfs-sdk'; +import { FSComponent, MutableSubscribable, Subject, VNode } from '@microsoft/msfs-sdk'; import { UiControl, UiControlProps } from '../UiControl'; @@ -17,7 +17,7 @@ interface NumberInputProps extends UiControlProps { /** Whether to wrap values from maxValue to minValue and vice versa. */ wrap: boolean; /** The value to increment or decrement with each inner knob input. */ - dataSubject: Subject; + dataSubject: MutableSubscribable; /** The optional default text value for when the dataSubject = 0. */ defaultDisplayValue?: string; /** An optional formatter for the display value */ diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/SelectControl.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/SelectControl.tsx index c2b6507a2..97f2fefa7 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/SelectControl.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/SelectControl.tsx @@ -164,4 +164,4 @@ export class SelectControl extends UiControl> { ); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/WaypointInput.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/WaypointInput.tsx index e1cbd3bcd..1c6b31e44 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/WaypointInput.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UIControls/WaypointInput.tsx @@ -90,7 +90,7 @@ export class WaypointInput extends UiControlGroup { public render(): VNode { return (
-
{this.store.displayWaypoint.city}
diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/ArrowControl.css b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/ArrowControl.css new file mode 100644 index 000000000..5d479c180 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/ArrowControl.css @@ -0,0 +1,27 @@ +.arrow-control { + position: relative; + display: flex; + align-items: baseline; + flex-direction: row; +} + +.arrow-control-arrow { + height: var(--arrow-control-arrow-height, 0.75em); + transform-origin: 50% 50%; + transform: scale(var(--arrow-control-arrow-scale, 0.85)); + fill: rgb(50, 50, 50); +} + +.arrow-control-arrow.arrow-control-arrow-enabled { + fill: rgb(0, 255, 0); +} + +.arrow-control-value { + color: var(--arrow-control-value-color, cyan); + margin-left: 4px; + margin-right: 4px; +} + +.arrow-control-value.highlight-active { + color: black; +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/ArrowControl.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/ArrowControl.tsx new file mode 100644 index 000000000..e8a40e45b --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/ArrowControl.tsx @@ -0,0 +1,201 @@ +import { DebounceTimer, FSComponent, MutableSubscribable, Subscribable, SubscribableUtils, VNode } from '@microsoft/msfs-sdk'; + +import { G1000UiControl, G1000UiControlProps } from '../G1000UiControl'; + +import './ArrowControl.css'; + +/** + * Base component props for ArrowControl. + */ +interface BaseArrowControlProps extends G1000UiControlProps { + /** The options that can be selected using the control. */ + options: Iterable; + + /** + * A function which renders selected values. If not defined, then selected values will be rendered using the default + * `toString()` method. + */ + renderValue?: (value: T) => string; + + /** + * The duration, in milliseconds, of the applied solid highlight when the control is focused or edited. Defaults to + * 1000. + */ + solidHighlightDuration?: number; + + /** CSS class(es) to apply to the root of the component. */ + class?: string; +} + +/** + * Component props for a ArrowControl that is bound to a subscribable value. + */ +interface SubscribableArrowControlProps extends BaseArrowControlProps { + /** The value to bind to the control. */ + value: Subscribable; + + /** + * A function which is called when a user selects an option using the control. + */ + onOptionSelected: (option: T) => void; +} + +/** + * Component props for a ArrowControl that is bound to a mutable subscribable value. + */ +interface MutableSubscribableArrowControlProps extends BaseArrowControlProps { + /** The value to bind to the control. */ + value: MutableSubscribable; + + /** + * A function which is called when a user selects an option using the control. If not defined, then the selected + * value will be written to the mutable subscribable bound to the control. + */ + onOptionSelected?: (option: T) => void; +} + +/** + * Component props for ArrowControl. + */ +export type ArrowControlProps = SubscribableArrowControlProps | MutableSubscribableArrowControlProps; + +/** + * A control which allows the user to select a value by scrolling left and right through a virtual list of options. + * The control displays the currently selected value and arrows to the left and right that depict whether the user can + * scroll left and/or right. + */ +export class ArrowControl extends G1000UiControl> { + private static readonly DEFAULT_SOLID_HIGHLIGHT_DURATION = 1000; // milliseconds + + private readonly valueRef = FSComponent.createRef(); + + private readonly options = Array.from(this.props.options); + private readonly selectedIndex = this.props.value.map(value => this.options.indexOf(value)); + + private readonly selectedValueDisplay = this.props.value.map(value => { + return this.props.renderValue ? this.props.renderValue(value) : `${value}`; + }); + + protected readonly solidHighlightTimer = new DebounceTimer(); + + /** @inheritDoc */ + protected onFocused(source: G1000UiControl): void { + super.onFocused(source); + + this.applySolidHighlight(this.props.solidHighlightDuration ?? ArrowControl.DEFAULT_SOLID_HIGHLIGHT_DURATION); + } + + /** @inheritDoc */ + protected onBlurred(source: G1000UiControl): void { + super.onBlurred(source); + + this.valueRef.instance.classList.remove('highlight-active'); + this.valueRef.instance.classList.remove('highlight-select'); + this.solidHighlightTimer.clear(); + } + + /** @inheritDoc */ + protected onEnabled(source: G1000UiControl): void { + super.onEnabled(source); + + this.valueRef.instance.classList.remove('input-disabled'); + } + + /** @inheritDoc */ + protected onDisabled(source: G1000UiControl): void { + super.onDisabled(source); + + this.valueRef.instance.classList.add('input-disabled'); + } + + /** + * Applies a solid highlight to this control's value display. + * @param duration The duration, in milliseconds, of the highlight. + */ + protected applySolidHighlight(duration: number): void { + this.valueRef.instance.classList.remove('highlight-select'); + this.valueRef.instance.classList.add('highlight-active'); + + this.solidHighlightTimer.schedule(() => { + this.valueRef.instance.classList.remove('highlight-active'); + if (this.isFocused) { + this.valueRef.instance.classList.add('highlight-select'); + } + }, duration); + } + + /** @inheritDoc */ + public onUpperKnobInc(): boolean { + this.changeOption(1); + return true; + } + + /** @inheritDoc */ + public onUpperKnobDec(): boolean { + this.changeOption(-1); + return true; + } + + /** + * Changes this control's selected option. + * @param direction The direction in which to change the selected option. + */ + private changeOption(direction: 1 | -1): void { + if (this.options.length > 0) { + let index = this.selectedIndex.get(); + + if (index < 0) { + index = direction === 1 ? -1 : this.options.length; + } + + const newIndex = index + direction; + if (newIndex >= 0 && newIndex < this.options.length) { + const selected = this.options[newIndex]; + + if (this.props.onOptionSelected) { + this.props.onOptionSelected(selected); + } else if (SubscribableUtils.isMutableSubscribable(this.props.value)) { + this.props.value.set(selected); + } + } + } + + this.applySolidHighlight(this.props.solidHighlightDuration ?? ArrowControl.DEFAULT_SOLID_HIGHLIGHT_DURATION); + } + + /** @inheritDoc */ + public render(): VNode { + return ( +
+ index < 0 || index > 0) + }} + > + + +
{this.selectedValueDisplay}
+ index < this.options.length - 1) + }} + > + + +
+ ); + } + + /** @inheritDoc */ + public destroy(): void { + this.solidHighlightTimer.clear(); + this.selectedIndex.destroy(); + this.selectedValueDisplay.destroy(); + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/CourseNumberInput.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/CourseNumberInput.tsx new file mode 100644 index 000000000..7d35e634a --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/CourseNumberInput.tsx @@ -0,0 +1,58 @@ +import { FocusPosition } from '@microsoft/msfs-sdk'; + +import { GenericNumberInput } from './GenericNumberInput'; + +/** + * A component for course number inputs. + * A variation of {@link GenericNumberInput} that is used for course inputs. + * Differs from {@link GenericNumberInput} in the keypad entry logic. + **/ +export class CourseNumberInput extends GenericNumberInput { + /** @inheritDoc */ + protected handleDigitInput(digit: number): void { + if (!this.isEditing) { + this.activateEditing(undefined, FocusPosition.First); + this.inputValue = digit * 10; + this.digitValues[0].set(digit * 10); + this.digitValues[1].set(0); + // if we entered a number that couldn't be a valid first digit of a course, we scroll to the second input + if (digit > 3) { + this.inputGroupRef.instance.scroll('forward'); + } + } else { + const focusedIndex = this.inputGroupRef.instance.getFocusedIndex(); + if (focusedIndex < 1) { + // the first input is a double digit input, + // so if its scaled value is 30 or less, if the value would be valid as the first digit of a course, + // we can set the current value to the hundreds place and set the digit as the tens place + // and scroll to the next input + const currentDigitValue = this.digitValues[0].get(); + if ((currentDigitValue === 30 && digit <= 6) || currentDigitValue < 30) { + this.digitValues[0].set((this.digitValues[0].get() + digit) * 10); + this.inputGroupRef.instance.scroll('forward'); + } else { // we reset the value the digit value + this.digitValues[0].set(digit * 10); + this.inputValue = digit * 10 + this.digitValues[1].get(); + // if we entered a number that couldn't be a valid first digit of a course, we scroll to the second input + if (digit > 3) { + this.inputGroupRef.instance.scroll('forward'); + } + } + } else { + // when editing the last digit: + // if current value starts with 36, if we enter a number that's not zero, we need to set the first digit to 0 + // and the second digit to the number we entered + // otherwise, we just set the second digit to the number we entered + if (this.digitValues[0].get() === 360 && digit !== 0) { + this.digitValues[0].set(0); + this.digitValues[1].set(digit); + } else { + this.digitValues[1].set(digit); + } + this.inputValue = this.digitValues[0].get() + digit; + this.deactivateEditing(true); + } + } + } + +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/G1000UiControlWrapper.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/G1000UiControlWrapper.tsx index 821ea01dc..e76f5feda 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/G1000UiControlWrapper.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/G1000UiControlWrapper.tsx @@ -1,7 +1,7 @@ import { FocusPosition, FSComponent, VNode } from '@microsoft/msfs-sdk'; import { FmsHEvent } from '../FmsHEvent'; -import { G1000UiControl } from '../G1000UiControl'; +import { G1000ControlList, G1000UiControl } from '../G1000UiControl'; import { EntryDirection, UiControlGroup } from '../UiControlGroup'; /** @@ -14,7 +14,7 @@ export class G1000UiControlWrapper extends UiControlGroup { /** @inheritdoc */ public onAfterRender(thisNode: VNode): void { FSComponent.visitNodes(thisNode, (node) => { - if (node.instance instanceof G1000UiControl) { + if (node.instance instanceof G1000UiControl || node.instance instanceof G1000ControlList) { this.control = node.instance; return true; } diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/GenericNumberInput.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/GenericNumberInput.tsx index a3ba5ce39..364d75c14 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/GenericNumberInput.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/GenericNumberInput.tsx @@ -53,6 +53,9 @@ export interface GenericNumberInputProps extends G1000UiControlProps { /** CSS class(es) to apply to the root of the component. */ class?: string; + + /** Whether keyboard entry should be disabled. */ + keyboardEntryDisabled?: boolean; } /** @@ -64,15 +67,15 @@ export class GenericNumberInput extends G1000UiControl private static readonly DEFAULT_SOLID_HIGHLIGHT_DURATION = 1000; // milliseconds private readonly rootRef = FSComponent.createRef(); - private readonly inputGroupRef = FSComponent.createRef(); + protected readonly inputGroupRef = FSComponent.createRef(); private readonly activeRef = FSComponent.createRef(); private readonly inactiveRef = FSComponent.createRef(); - private readonly signValues: Subject<1 | -1>[] = []; - private readonly digitValues: Subject[] = []; + protected readonly signValues: Subject<1 | -1>[] = []; + protected readonly digitValues: Subject[] = []; - private isEditing = false; - private inputValue = 0; + protected isEditing = false; + protected inputValue = 0; private renderedInactiveValue: string | VNode | null = null; private readonly solidHighlightTimer = new DebounceTimer(); @@ -268,11 +271,88 @@ export class GenericNumberInput extends G1000UiControl return handled || !!(this.props.onClr && this.props.onClr(source)); } + /** + * Consolidates a keyboard event into a digit input event. + * @param source The source of the event. + * @param evt The event. + * @returns Whether the event was handled. + */ + public consolidateKeyboardHEvent(source: G1000UiControl, evt: FmsHEvent): boolean { + const digit = parseInt(evt); + if (isNaN(digit)) { + return false; + } + + if (this.props.keyboardEntryDisabled) { + return true; + } + + this.handleDigitInput(digit); + return true; + } + + /** + * Responds to a digit input event. + * @param digit The digit that was input. + */ + protected handleDigitInput(digit: number): void { + if (!this.isEditing) { + this.activateEditing(undefined, FocusPosition.Last); + this.inputValue = digit; + this.digitValues.forEach((value, index) => { + if (index === this.digitValues.length - 1) { + value.set(digit); + } else { + value.set(0); + } + }); + } else { + const focusedIndex = this.inputGroupRef.instance.getFocusedIndex(); + if (focusedIndex < this.digitValues.length - 1) { + // subtract the scaled value of the old digit from the total value and add the scaled value of the new digit + // to keep the input value up-to-date + this.inputValue = this.inputValue + - this.calculateScaledValue(this.digitValues[focusedIndex].get(), focusedIndex, this.digitValues.length) + + this.calculateScaledValue(digit, focusedIndex, this.digitValues.length); + this.digitValues[focusedIndex].set(this.calculateScaledValue(digit, focusedIndex, this.digitValues.length)); + this.inputGroupRef.instance.scroll('forward'); + } else { + // if the current value already uses all digits, only reset the last digit + if (this.inputValue > (10 ** (this.digitValues.length - 1))) { + this.digitValues[focusedIndex].set(this.calculateScaledValue(digit, focusedIndex, this.digitValues.length)); + } else { + // otherwise, shift all digits to the left and add the digit to the value + this.inputValue = this.inputValue * 10 + digit; + this.digitValues.forEach((value, index) => { + // set the digit to the value of the input if it's the last digit, otherwise set it to the next digit's current value + if (index === this.digitValues.length - 1) { + value.set(digit); + } else { + value.set(this.digitValues[index + 1].get() * 10); + } + }); + } + } + } + } + + /** + * Calculates the scaled value of a digit. + * @param digit The digit. + * @param index The index of the digit. + * @param length The total number of digits. + * @returns The scaled value of the digit. + */ + protected calculateScaledValue(digit: number, index: number, length: number): number { + return digit * (10 ** (length - index - 1)); + } + /** * Activates editing for this component. * @param activatingEvent The event that triggered activation of editing, if any. + * @param focusPosition The position to focus when editing is activated. Defaults to First. */ - private activateEditing(activatingEvent?: FmsHEvent.UPPER_INC | FmsHEvent.UPPER_DEC): void { + protected activateEditing(activatingEvent?: FmsHEvent.UPPER_INC | FmsHEvent.UPPER_DEC, focusPosition?: FocusPosition): void { if (this.isEditing) { return; } @@ -285,7 +365,7 @@ export class GenericNumberInput extends G1000UiControl this.activeRef.instance.style.display = ''; this.inputGroupRef.instance.setDisabled(false); - this.inputGroupRef.instance.focus(FocusPosition.First); + this.inputGroupRef.instance.focus(focusPosition ?? FocusPosition.First); if (activatingEvent !== undefined && this.props.editOnActivate) { this.inputGroupRef.instance.onInteractionEvent(activatingEvent); @@ -298,7 +378,7 @@ export class GenericNumberInput extends G1000UiControl * Deactivates editing for this component. * @param saveValue Whether to save the current edited input value to this component's bound value. */ - private deactivateEditing(saveValue: boolean): void { + protected deactivateEditing(saveValue: boolean): void { if (!this.isEditing) { return; } @@ -363,4 +443,4 @@ export class GenericNumberInput extends G1000UiControl this.digitValues.forEach(value => value.unsub(this.inputChangedHandler)); this.cleanUpRenderedInactiveValue(); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/TimeNumberInput.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/TimeNumberInput.tsx new file mode 100644 index 000000000..a11051780 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/TimeNumberInput.tsx @@ -0,0 +1,59 @@ +import { FocusPosition, UnitType } from '@microsoft/msfs-sdk'; + +import { GenericNumberInput } from './GenericNumberInput'; + +/** + * A component for time number inputs. + * A variation of {@link GenericNumberInput} that is used for time inputs. + * Differs from {@link GenericNumberInput} in the keypad entry logic. + **/ +export class TimeNumberInput extends GenericNumberInput { + private static readonly HR_TO_MS = UnitType.HOUR.convertTo(1, UnitType.MILLISECOND); + private static readonly MIN_TO_MS = UnitType.MINUTE.convertTo(1, UnitType.MILLISECOND); + + /** @inheritDoc */ + protected handleDigitInput(digit: number): void { + if (!this.isEditing) { + this.activateEditing(undefined, FocusPosition.First); + if (this.signValues.length > 0) { + // if we have a sign value, we need to scroll to the first digit input (assuming that any sign values are first) + this.inputGroupRef.instance.setFocusedIndex(this.signValues.length + (digit > 2 ? 1 : 0)); + } + this.digitValues[0].set(digit * TimeNumberInput.HR_TO_MS); + this.digitValues[1].set(0); + this.digitValues[2].set(0); + } else { + const focusedIndex = this.inputGroupRef.instance.getFocusedIndex(); + if (focusedIndex <= 1) { + // sign input or hours input + // if the sign is focused, we treat the digit input as hours input and set focus there + if (focusedIndex === 0) { + this.inputGroupRef.instance.setFocusedIndex(this.signValues.length); + } + // the hour input is a double-digit input, max value 23 + // if the current value is 1 or 2, we insert the input as the second digit of this value + const hoursValue = this.digitValues[0].get() / TimeNumberInput.HR_TO_MS; + if (hoursValue < 2 || hoursValue < 3 && digit < 4) { + this.digitValues[0].set((hoursValue * 10 + digit) * TimeNumberInput.HR_TO_MS); + this.inputGroupRef.instance.scroll('forward'); + } else { + this.digitValues[0].set(digit * TimeNumberInput.HR_TO_MS); + if (digit > 2) { + this.inputGroupRef.instance.scroll('forward'); + } + } + } else if (focusedIndex === 2) { + // minutes tens input + if (digit < 6) { + this.digitValues[1].set(digit * 10 * TimeNumberInput.MIN_TO_MS); + this.inputGroupRef.instance.scroll('forward'); + } + } else { + // minutes ones input + this.digitValues[2].set(digit * TimeNumberInput.MIN_TO_MS); + this.deactivateEditing(true); + } + this.inputValue = this.digitValues[0].get() + this.digitValues[1].get() + this.digitValues[2].get(); + } + } +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/index.ts index 323d041a5..e5a32854a 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/index.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UiControls2/index.ts @@ -1,5 +1,8 @@ +export * from './ArrowControl'; +export * from './CourseNumberInput'; export * from './DigitInput'; export * from './G1000UiControlWrapper'; export * from './GenericNumberInput'; export * from './SelectControl'; export * from './SignInput'; +export * from './TimeNumberInput'; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UserSettings/UserSettingSelectControl.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UserSettings/UserSettingSelectControl.tsx index 3115288cb..635e7b4f3 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UserSettings/UserSettingSelectControl.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/UserSettings/UserSettingSelectControl.tsx @@ -89,4 +89,4 @@ export class UserSettingSelectControl< /> ); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/ViewService.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/ViewService.ts index 30e251d5f..31d9039fe 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/ViewService.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/ViewService.ts @@ -1,5 +1,6 @@ import { EventBus, FSComponent, HEvent, NodeReference, Subject, Subscribable, VNode } from '@microsoft/msfs-sdk'; +import { ControlpadInputController } from './Controllers/ControlpadInputController'; import { FmsHEvent } from './FmsHEvent'; import { UiPage } from './UiPage'; import { UiView } from './UiView'; @@ -17,7 +18,6 @@ type ViewEntry = { * A service to manage views. */ export abstract class ViewService { - private readonly registeredViews: Map VNode> = new Map(); private readonly refsMap: Map = new Map(); @@ -59,14 +59,17 @@ export abstract class ViewService { /** * Routes the HEvents to the views. * @param hEvent The event identifier. + * @returns whether the event was handled */ - protected onInteractionEvent(hEvent: string): void { - // console.log(hEvent); - - const evt = this.fmsEventMap.get(hEvent); + protected onInteractionEvent(hEvent: string): boolean { + let evt = ControlpadInputController.controlPadEventMap.get(hEvent); + if (evt === undefined) { + evt = this.fmsEventMap.get(hEvent); + } if (evt !== undefined) { - this.routeInteractionEventToViews(evt); + return this.routeInteractionEventToViews(evt); } + return false; } /** @@ -80,7 +83,6 @@ export abstract class ViewService { if (activeView) { return activeView.processHEvent(evt); } - return false; } @@ -265,4 +267,4 @@ export abstract class ViewService { this.openViews.length = 0; } } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/WptInfo/WptInfo.tsx b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/WptInfo/WptInfo.tsx index 209640d9f..371cbc088 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/WptInfo/WptInfo.tsx +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/WptInfo/WptInfo.tsx @@ -97,6 +97,7 @@ export abstract class WptInfo extends UiV protected onViewClosed(): void { this.planePosConsumer.off(this.planePosHandler); this.planeHeadingConsumer.off(this.planeHeadingHandler); + this.inputSelectedIcao.set(''); // Clean up the icao subject, otherwise the subject's subscriptions are notified when the view opens the next time. } /** diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Units/UnitsUserSettings.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Units/UnitsUserSettings.ts index b9a5c689a..bff64b43b 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Units/UnitsUserSettings.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/Units/UnitsUserSettings.ts @@ -5,5 +5,5 @@ import { export { UnitsAltitudeSettingMode, UnitsDistanceSettingMode, UnitsNavAngleSettingMode, UnitsTemperatureSettingMode, - UnitsUserSettings, UnitsUserSettingManager, type UnitsUserSettingTypes, UnitsWeightSettingMode + UnitsUserSettings, type UnitsUserSettingManager, type UnitsUserSettingTypes, UnitsWeightSettingMode }; \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeed.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeed.ts new file mode 100644 index 000000000..3881c3787 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeed.ts @@ -0,0 +1,44 @@ +/** + * A definition for a reference V-speed. + */ +export type VSpeedDefinition = { + /** The name of the V-speed. */ + readonly name: string; + + /** The default value of the V-speed, in knots. */ + readonly defaultValue: number; + + /** The label text to display for the V-speed in the TimerRef menu. */ + readonly label: string; +} + +/** + * Base type for V-speed groups. + */ +export type VSpeedGroup = { + /** This group's name. */ + readonly name: string; + + /** Definitions for each reference V-speed contained in this group. */ + readonly vSpeedDefinitions: readonly VSpeedDefinition[]; +}; + +/** + * Keys for reference V-speed values derived from aircraft configuration files. + */ +export enum VSpeedValueKey { + StallLanding = 'VS0', + StallCruise = 'VS1', + FlapsExtended = 'VFe', + NeverExceed = 'VNe', + NormalOperation = 'VNo', + Minimum = 'VMin', + Maximum = 'VMax', + Rotation = 'Vr', + BestClimbAngle = 'Vx', + BestClimbRate = 'Vy', + Approach = 'Vapp', + BestGlide = 'BestGlide', + BestClimbRateSingleEngine = 'Vyse', + MinimumControl = 'Vmc' +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedConfig.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedConfig.ts new file mode 100644 index 000000000..513b8c4c4 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedConfig.ts @@ -0,0 +1,61 @@ +import { ResolvableConfig } from '../Config/Config'; +import { VSpeedDefinition, VSpeedValueKey } from './VSpeed'; + +/** + * A configuration object which defines a reference V-speed. + */ +export class VSpeedConfig implements ResolvableConfig { + public readonly isResolvableConfig = true; + + /** The name of this config's reference V-speed. */ + public readonly name: string; + + /** The default value of this config's reference V-speed. */ + public readonly defaultValue: number | VSpeedValueKey; + + /** The label text of this config's reference V-speed. */ + public readonly label: string; + + /** + * Creates a new VSpeedConfig from a configuration document element. + * @param element A configuration document element. + */ + public constructor(element: Element) { + if (element.tagName !== 'VSpeed') { + throw new Error(`Invalid VSpeedConfig definition: expected tag name 'VSpeed' but was '${element.tagName}'`); + } + + const name = element.getAttribute('name'); + if (name === null) { + throw new Error('Invalid VSpeedConfig definition: undefined name'); + } + this.name = name; + + const value = element.textContent; + if (value === null) { + throw new Error('Invalid VSpeedConfig definition: undefined value'); + } + + const numberValue = Number(value); + if (!isNaN(numberValue)) { + this.defaultValue = numberValue < 1 ? -1 : Math.round(numberValue); + } else if (Object.values(VSpeedValueKey).includes(value as any)) { + this.defaultValue = value as VSpeedValueKey; + } else { + throw new Error(`Invalid VSpeedConfig definition: unrecognized value ${value} (value must be a number or a valid reference speed key)`); + } + + this.label = element.getAttribute('label') ?? `V${name}`; + } + + /** @inheritdoc */ + public resolve(): VSpeedDefinition { + return { + name: this.name, + defaultValue: typeof this.defaultValue === 'number' + ? this.defaultValue + : Math.round(Simplane.getDesignSpeeds()[this.defaultValue]), + label: this.label + }; + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedGroupConfig.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedGroupConfig.ts new file mode 100644 index 000000000..496cc79ed --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedGroupConfig.ts @@ -0,0 +1,47 @@ +import { ResolvableConfig } from '../Config/Config'; +import { VSpeedDefinition, VSpeedGroup } from './VSpeed'; +import { VSpeedConfig } from './VSpeedConfig'; + +/** + * A configuration object which defines an airspeed tape color range. + */ +export class VSpeedGroupConfig implements ResolvableConfig { + /** @inheritdoc */ + public readonly isResolvableConfig = true; + + /** The name of the group defined by this configuration object. */ + public readonly name: string; + + /** The V-speeds included in the group defined by this configuration object. */ + public readonly vSpeedDefinitions: readonly VSpeedDefinition[]; + + /** + * Creates a new VSpeedGroupConfig from a configuration document element. + * @param element A configuration document element. + */ + constructor(element: Element) { + if (element.tagName !== 'Group') { + throw new Error(`Invalid VSpeedGroupConfig definition: expected tag name 'Group' but was '${element.tagName}'`); + } + + this.name = element.getAttribute('name') ?? ''; + + const children = Array.from(element.querySelectorAll(':scope>VSpeed')); + this.vSpeedDefinitions = children.map(child => { + try { + return new VSpeedConfig(child).resolve(); + } catch (e) { + console.warn(e); + return null; + } + }).filter(val => val !== null) as VSpeedDefinition[]; + } + + /** @inheritdoc */ + public resolve(): VSpeedGroup { + return { + name: this.name, + vSpeedDefinitions: Array.from(this.vSpeedDefinitions) + }; + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedUserSettings.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedUserSettings.ts new file mode 100644 index 000000000..f94de207f --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/VSpeedUserSettings.ts @@ -0,0 +1,87 @@ +import { + Consumer, DefaultUserSettingManager, EventBus, UserSetting, UserSettingDefinition, UserSettingManager, + UserSettingMap, UserSettingRecord, UserSettingValue +} from '@microsoft/msfs-sdk'; + +import { VSpeedUserSettingTypes } from '@microsoft/msfs-garminsdk'; +import { VSpeedGroup } from './VSpeed'; + +/** + * A manager for reference V-speed user settings. + */ +export class VSpeedUserSettingManager implements UserSettingManager { + /** An map of groups (keyed on group type) containing the reference V-speeds for which this manager contains settings. */ + public readonly vSpeedGroups: ReadonlyMap; + + private readonly manager: DefaultUserSettingManager; + + /** + * Creates a new instance of VSpeedUserSettingManager. + * @param bus The event bus. + * @param vSpeedGroups Definitions for each reference V-speed for which to create settings, organized into groups. + */ + public constructor(bus: EventBus, vSpeedGroups: ReadonlyMap) { + const groupsCopy = new Map(); + const settingDefs: UserSettingDefinition[] = []; + + for (const group of vSpeedGroups.values()) { + groupsCopy.set(group.name, { + name: group.name, + vSpeedDefinitions: Array.from(group.vSpeedDefinitions) + }); + + for (const vSpeed of group.vSpeedDefinitions) { + settingDefs.push( + { + name: `vSpeedShow_${vSpeed.name}`, + defaultValue: true + }, + { + name: `vSpeedDefaultValue_${vSpeed.name}`, + defaultValue: vSpeed.defaultValue + }, + { + name: `vSpeedUserValue_${vSpeed.name}`, + defaultValue: -1 + }, + { + name: `vSpeedFmsValue_${vSpeed.name}`, + defaultValue: -1 + }, + { + name: `vSpeedFmsConfigMiscompare_${vSpeed.name}`, + defaultValue: false + } + ); + } + } + + this.vSpeedGroups = groupsCopy; + this.manager = new DefaultUserSettingManager(bus, settingDefs); + } + + /** @inheritdoc */ + public tryGetSetting(name: K): K extends keyof VSpeedUserSettingTypes ? UserSetting : undefined { + return this.manager.tryGetSetting(name) as any; + } + + /** @inheritdoc */ + public getSetting(name: K): UserSetting> { + return this.manager.getSetting(name); + } + + /** @inheritdoc */ + public whenSettingChanged(name: K): Consumer> { + return this.manager.whenSettingChanged(name); + } + + /** @inheritdoc */ + public getAllSettings(): UserSetting[] { + return this.manager.getAllSettings(); + } + + /** @inheritdoc */ + public mapTo(map: UserSettingMap): UserSettingManager { + return this.manager.mapTo(map); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/index.ts new file mode 100644 index 000000000..633b10219 --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/VSpeed/index.ts @@ -0,0 +1,3 @@ +export * from './VSpeed'; +export * from './VSpeedConfig'; +export * from './VSpeedUserSettings'; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/index.ts b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/index.ts index e3dd16996..50bfac701 100644 --- a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/index.ts +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/index.ts @@ -1,5 +1,6 @@ export * from './Autopilot'; export * from './Backlight'; +export * from './Config'; export * from './DateTime'; export * from './Input'; export * from './Instruments'; @@ -11,9 +12,12 @@ export * from './Systems'; export * from './Traffic'; export * from './UI'; export * from './Units'; +export * from './VSpeed'; + export * from './FlightPlanAsoboSync'; export * from './FuelComputer'; export * from './G1000Events'; +export * from './G1000PfdPlugin'; export * from './G1000Plugin'; export * from './NavComConfig'; export * from './NavProcessorConfig'; diff --git a/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/package.json b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/package.json new file mode 100644 index 000000000..a1994f86a --- /dev/null +++ b/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/package.json @@ -0,0 +1,18 @@ +{ + "name": "@microsoft/msfs-wtg1000", + "version": "1.2.2", + "description": "Garmin G1000 NXi", + "main": "../html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/index.ts", + "scripts": {}, + "repository": { + "type": "git", + "url": "https://workingtitlesim@dev.azure.com/workingtitlesim/KittyHawk/_git/ProjectGolf" + }, + "author": "Working Title Simulations, LLC", + "license": "Modified MIT", + "devDependencies": { + "@microsoft/msfs-sdk": "*", + "@microsoft/msfs-types": "*", + "@microsoft/msfs-garminsdk": "*" + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g1000/package.json b/src/workingtitle-instruments-g1000/package.json index 26510916b..d01bf0d08 100644 --- a/src/workingtitle-instruments-g1000/package.json +++ b/src/workingtitle-instruments-g1000/package.json @@ -1,6 +1,6 @@ { "name": "workingtitle-instruments-g1000", - "version": "1.2.7", + "version": "1.3.3", "description": "Working Title Garmin G1000 NXi", "main": "index.ts", "scripts": { @@ -28,4 +28,4 @@ "rollup-plugin-import-css": "^3.1.0", "typescript": "4.5.5" } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-g1000/rollup-defs.config.js b/src/workingtitle-instruments-g1000/rollup-defs.config.js deleted file mode 100644 index b7e9b34f7..000000000 --- a/src/workingtitle-instruments-g1000/rollup-defs.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import dts from 'rollup-plugin-dts' -import css from 'rollup-plugin-import-css'; -import resolve from '@rollup/plugin-node-resolve'; - -const config = [ - { - input: 'build/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/index.d.ts', - output: [{ file: 'dist/index.d.ts', format: 'es' }], - plugins: [dts(), resolve(), css({ output: 'ignoreme.css' })], - } -] -export default config \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Assets/Fonts/DejaVuSans-SemiBold.ttf b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Assets/Fonts/DejaVuSans-SemiBold.ttf index 037389173c31394f7d9d4cd8915c00c845283c17..a95e6e903ba20cc41c8e7a6741c002a2cbd13585 100644 GIT binary patch delta 18544 zcmb_^3w#yD75B_OZyu000YgYaLM{*>;VlM*A_asHP-s9v2@e4QF%$$u^wMf;5L(cU zti=daQ&5T;Nyt))H5GiI)JLVDfQld*LKH+SAb9utpP9YM4bZRse!q|3Z}04$IWu$K zbLQ-Z()YWZJJ=<_IAbgxKN?FPGGy$?4cpHiWlHA$Cy4#2uitg$LR|dCnv}B|AD3X(8t(k$H&j3_UNVe z2U?@~?i*v(m)J(5nk~_j*(2sUR-k{uCMrGIMm3E+hHDyR7lHn2WhI-Z9A`J7tx>kQ z{{a3}YqXLan2oz%;$)$ex?=?}6BeJv|AI-zaJDztU1 z!sv--ja4X<@EprV=>yQ-2l&^(xs0}e6>1-ecBH-uod6FgS z54h(6+^#>s`Vi?vBHCv`#BvZe5p0atzGg)jT&B$cn~AJIYhVS+hb)rr^>|jM^kd~p zKetAER^FTMvND2eCj@>O;i<#%oWq8znJgZCrCQW3W*RG52W^*o#wt;hd9_ulZU__- zci|Vf%g6PsGRY01HCpDfUxm0M4x3%oybYdb2o8x$?;-}ZgKV!J$#xPm*SgHZ>;Y}Y zO_Dp}Fm!>l`@rEEw4dQR2d?IaxFZh3FY8M!Cs0HR6EX|6P@It6e`tkLAtS+BB4#HA zgM$Cok^zuxo3|~)0I&|LT=xemz`$Cv6SRNVc3}EnvU0K+Fmzbh4O9p9?Iimlb$V?n zwJP(TccBfqXO$sLAq|iau5688vwr}#7kxX0{XsxR!JXh=+{o6qnl13y0=}H&1l*VcN<57TW9#Q04Y6QV8Xc`=zNjYM!p41MGLZcO&^bS( z11U`RWN2il)yJ}p+6FeB1m27iPlvv*A+-B&g#>#)izORsTnA>Yod~dDArc$gD~tuC zZ^k0Kp=}-Q@el!__9|hMkH8^?uTbj=JtSORgVwvSp%iI!>o*bYyB3A)GBo9XYDo)K z`y))1PqdCiSLqWOyFAeG@NKu2}mKHOL7g%Fsdg8YDq1fd8N=NlwJ(6PRw#TbQDv3H=C-IHs4Rr}(4@;DJ|u-uPg3SvAqVJMg#pOBwX z-e@+SrHAZ`h!=!ZC*!$H}+fwe;bg>6n3)6BVD)bAGOj}k_wKc6u0R_eFpT_Uy_6~$zotcB* zqnQJ^S(TpkIfrY}9tZBPYQ3--3X}h>4YBgyTG~Tt8m#%#+H;`*dSxqu=t#C1t*VT4e-h8MgNPvOen!Sjd!=Xtp&QyfLqXs_v}F0= z0rkpf;Spp<r{QPE|c>HKGeXL|Z0E3K-?>C54`N4kvYy#RnGVGb0hcGA7LNMX2)-R>6S5AGL8 zCQXqzGIo$!YhX^IRnqkV#Qlb@t8ZVg9%J*6-CF3IXv}jr)1@$X3$on^z&_ZXSrvW%0dsl?Aji=IhJV#jjyUmOn$LF!7YHv0G*9=^ftb$yB zn{l{Xf$L6OQ*kx*uF8Fl$6Ez?uYtqp?r#Mu)HCp!N8p-M(IQNmQ`|$oAKXKcD{wy! zFzhYnV)uMR3#86)1|oVN*Yj5Fu$!yi>OEK0 ze+^AMVGZn;ZjJ8et19jn6{{B-=U8D?$u0S$wXMU)@ro*K!~;A~K>-2R5DKB>ySRQ7 z;g7c00q(!F);I)(Ha={@6e4l$CDte?33d?+D4^iV7uz*NM3E8mJcsdno1H2iWRH^w z<+d(&(uc_($o8%1kq>xV&8m@Ae0bHs!f}y-MsW&v3OnYgtC;=&e$2W0lp`{8Zsp75+ z+DOI#*P>hD%(@)MbdxP-4=E>=ZUZ znqQkgm_N20(DHXJ?Uoy?A=A4DzGyYH)uLAAsfnqXsohd1rp``%F!eX7uchv7cPT9? zEj8`uX(Q9drQMPCU|MzBp0tnB4yTD$v^P5&_cNctBU5gE5+ z6lVM;V?}0MW}D26%#N8|GVjZLF!RyOvdot<-_NYe%FMbuyF7bG_U`Os{s_O{Kf+%W z>%Y@q=6~7$H~*XdqyAGJT`TLZuD5pR)@`I);cgnbY22oHn|`G24pIjHG?{v0NX;8jb znR=`B-d!0Tl8h81!x(6cGm0US`;9jtk~3zldCI&1k^HJs)>2f#bah{_m|F*;*K z#+uC5nQb$(GIKH)WG)rFKb`qr=JBldS;ZY#_WJBv@Xr0w{!adp{_*}P{-^w}_}>8Q zU-{2;GOU&}y9TUtYQ#WWn@ciF?s?-Q(`-ZY^W(E4cpbu6N7br(D}z>ptl|=9apT zy1#X&xRc#+?rrGri>sU43BMU`JNG)q65Z(F*DHe_P*Bf2zK|zOMf3 z`V;kE)*r9`to}&-;rfr!_c|`8{txv}p)IR_ynbcPqW=Q};;SLv?r671trJvd+!wY%um^-kuHC zlrdRK+f!EeZmpxCeEj1}$v*&Z*O4#8?Lz|iYthHJ*LqKniN5tf?$(h%B7sf6G}D#; z_(C9GH=0ky%qULLKVBl+re8iIbT5igp$A5zfG>(fjX;8wVA9S3K3a5-=QVs6|0l2K zXZVjwE2WPzURkOvSAMIkRkkQQlzQb1V`_=IOnpRKqpj6m(spQn(~fAzAQGm$EGJef zRjw*3j^*@E)l?h^R6`%C4@2<2MIVme5&B5ks~76fG7B($xqe7LtREq;{;B>Mem}=` z)W|flP{r#&a3kCBUD{?gKo17>PwA zN8l=m`hC>us5R6R^>|cS6fBMY6-TX&vZIdCZPf7=f{zxJE%q|j;vIsucsJ(Bn6)u> z3~VUo4>9Xvo{lMx0jV*c#(Wm@dCXD5!P6IUlj826ySO{!Cdc1SckvVAC&o{Tzr()n znTWZQcV9jeF*uQ63E2t$gpLWF5;`a32%KK)BhnR& z8oNHCnp)q6h~DNa3EL8?5~}Ut8zKhx!JvZ)A18d0a46w0?m=F{rwNc_!siJ`@%u%> zv4rCZUnZPL_$uM+gt~;23H4&)nhg=PG~uF+5yR0swHqT^@>uFhJfCB&Mo*X~JwCYrXwtPkp1hMQ!=S2|w>|bv&_vKVg-haP$KAxLH^={wtI7pRD4q z9m3*eR@=H1zRSA4?q+>j@T^j1Ew3A(M<~P9^VXiap?WnG$E?_sIr=;|p4V9ePiD5M zP~O9?!xA!*GHz04{IpJa?=%SFtj1b)vadPSJ+4(7`PR;pckp&r>-wkpQ&rXVV>q8} z`5JcXGtgaWebDfhJ_&bQt@3XyOq=(u-&em}5R0w?teO|&ZpXKo9_;A1em>9g zowoF;!E$qL)yC7gTt5U;TyO0^)0c9v?^f!C?s2`qD*tXVo)gcG(<@N9uCz+d=7_st z*2c4bfm3PiJNq!f^kQr5_qiFk<2#IW$y;1T1y2k%80i;}5^tm2pgO~<_`avW-(wy5 zz9(jnJ(ot$#$c=0xqQ(x-dcDrSIi!?oO+&yVuRKXTrW^njTMEZ>Cz{Jb^n!eTzys=Y8re^#g^@8XDziKtEOVxJR%9g;G7 zYr8IO`1}yKCj!T7XO#U``Nb5y2y(rxYS+aaPAb=;t%e_G;;=mSk|mxitcFXAd89S{ z@=#t-we@lW*UK@%dTY;5e!V7Ggu638-OMjqy@EZxp1HvkKFBH!el8|hS2fp7;k@9< zQhrz;?jBbg>{*;Y$K&m2g*&9^nm-n|XsQj;J zsx`jBtme-fO#PEHLFXsp145JLocL^(#aOJFjQTFW4?EbE@XTiLdm8bxo5AmFgl9E_ z7dFD%6CC1%N9nW7dz`)`N#VZ=U{QU%Nvw1ii)Lv-u_zgNP!Q8Y z5~KUC3D0Z>Ct@Uib~8B5FX36u;DyeXwmeDUg?4Q_p4_HTEQpW}?lZM-#e%CqNTGcO z-MrB8rSZck#*hUXuVBu>KsDISx4}dqC<&j@2w!wUG7L`CN%*2`!~^5>Bg0_@ZmWmC|N#l7+xm)`!ELf^0rifuogm zrMQlYF{ynk&$w3doh@y;iISBLQ6E z6FZ%>KMHm*E`)Ck6Y{8DJQ36zl<0l3LJIw|_?v+9jf*{BpuO?{$z$=74K#0)C zk}!g1h8RfpYn%AJjRZ{rT;i`_W$q=wNnDkt)W;+qAc{JB3lwR~jTx|v(pe;3By|Gr z4ZuXeL)_@3r$~hd34a*izyv|-Jhzhgf@6J>;5dAKslVAmq8N8QQ558QrI8Ow9Mtc0 zzqx{-UV!gbkMd|q67Zn{z1S)4$}eG0wkPHCT-v4i4tq^5pPX9Z2KCpCbSSPXN)6<( zT?8BD!6Nm59i7KV=@r3t`X0MD4_0u|S(3+9-ksFC#+(Z^WvcV|BCM4wMMFc6OgTI(kX{sqCu zNw~WodTD7+#yr4`jWV&o0zMNK8t)|b<0!us+M{oPmfbhH`VOp-SWLZCX0t?iGEe6T4)%HM?C!g}pI*-h~fVEC(F0}_Z5gPr33 zd~sr%L3lHm3YSCDaE>IfPk)y80?w_@nbCYe5=BM66Ou5PR^~;$$R8+Z@Buoi5P%Db zkt;_03o*mb2kQR{gToqqH4?7X2>4LJ0G!i2FYLdo_PBscH>(I187lb;8;Q4qEPw?( z|AW^i$_~Cr4DcMu4f@`Y6X}x$9N87c226BB;_Eo8Vjc-EYrX*Bw-Ua$O_H#(fE^N+ z>7_b$ubF&IZ_kB*0}&FAmthH)6FVN9C)Q9_fIy;Ql}M;|%@o>gkWJfJ;;BG3%VzQX zks>Gd(au9l8_B=$tW6|YW_Q3h(Q?>-ZsYkOQp4~)E0jr@qutG;kon5sj@q2oG0$YtsOG*Bm})3T!Cy2cgOvuVGsh$nUs@ZIP|q z(ag3a?;?a_hoTS;84ztw>0G##7hCt=g9A;G-FhBh9FV7y5>EqWau@9cUy2bj&7BTr zYPF}_AZ8j&GkM0+rxCv+P9HZl;?v&$I$3WB-^&$fezBEf{y&NN$#5!+X(X*$ZKq2L z!5)o?APOWSw16DY5qmi;AO_g|@8t`oNMocqr7?EVf7UqXIz!UDLD1|eKS*HCu>2@=1;jxB+yR@ehecuz#Gh4fTtSCsG^KGpu9geP|&9!#Wo3+fNz z(*PQZFe<`lq6nV^A0yxhi=aNF%%r#!28qOmM78?)G3!2w%qHYiyU20VMj3;%!b)jFCKUcGv>;k|mIKjr05x zo;F#Zi0@K%Ir|Rs)v8|f)91l|I>m?i(^?x;5g4in2n~ZSV=xKevVb{>NBKBKpJ=pa zyX=xN>ts7BlR@Cy`vNv*(`V zDg6dh99P>0mlNY^SN5e`()|Xh(X(FPQqh;o>b<_R*k7{k+LL^8oNzMavmw=1*m?E5 zs)I0f0fwnVkbMw|28(C8nW0}Vwtc7gUz2c4WgW~QGdb$oV}E>#-%DMF9F-?#p|0`v zqy|1mmT>HS4LpTBU)yFkH1L(YrM=`EoN&)OW54C^M#^%ClXa2LF}(XSm%nU^5yPBe zrt)%h64?qJL%s8cD@rU*bfS{XWu?mLn53Nd?(M8(B`1{@6om$Li?Egs-ceh?ow>=% zDV`*f6J#?6u?_*C4=L>2HcA?w;jC(-+^l%I*q@@Tgg+LyRX!4QRXW$FDz-`zyo&c? zAtux`L#An;OIOAasP25J?y}Bu5ki3+3gD`eG%X5{7wNx4DYT~oicOVEHo+qGM z4cY|UN$;gx;z=SK6Y>>dgo$F7o!3W6k<~0`bRXq7Pu&h4?l)+vf5@rskW=Y6hSWHt zuUA&5r+S4SYz?C16K|+Bayf*JXy@{Ht`tb z4l)9H=i~%KeFK*5t8UMHZOokRp}oLC1h96P{v68*wHhUG*4D}GnG-&G=lac zvw$ojk6k@W*-d4&yA`;*{k&{GzFYYmV&5@a84;UQK&LANS%ZxgEQkcrY5jmQEe2(p zWsfNXRB7T)?eCO1Tz?Ve!%j}#YUQaeSU#@{^LnCp)WtQXVwTO=34t z{3h<5UVYWCk>EOQ`(YUW(7AAfdOj+S3=QWS2-M5#YKE$5bX-UureafZMh{bax8RMB{l8YDbgwFzXPeg4+xs}X|J%pL>=HLPYekS zP(ITtMMp(&QL_P+tV*&2cz8Xdl07YDOIy6%VD1 z`=N7HrMgm-ZTEO4@scnJkr}fx5OPt%Rap=~hAiZ=Lf<2_20T!(Uw>Y}%YrMkiwH4Z zCavtJbC1BUZHB*^f8ycS5`H&tfT65jGC!aw`I5;Df1*UW0|-W z&uB8sb!If+G7l?uw*|L>7>Qrhh=1qYFnl|@T1~m@$|)z#4nu)wgr=P6ZWoq*CH{n2 z&GG&4iNClS6aiAC{E%g=swVbeB-`v=xTQc-Ep*a)yJ&{VW(k@pvOXb=dWVpBA2E}x zPsrpEF#yJ1B;ZqF0w{n86L?Gr*251z<|X9&wGJZQc=)!jMosfx@pP$CQ)G$8H!e0i zdGePuFUB{mXzs<#JG8ERd%VFjn^yGP?rEYhqFCb7iuz{)-|xNy_!NSB6%onNF9k>5 z5diO~TbnEFH*3_SBr36x70(wecnJ0x&_`WRAwW1zul92Fcst#ZT^Lo$et9t)goj3r zMi-7Ja~%k`&@PwFKG|HBO_v>N&Rrr4H6O&}c$W?LT+hDYL!dMhn%ZtqRUR*tvFQ4H zgh1Rw2otoegz)!cb0H<0P{DTX&wvv?++0I_9`HnZ^JnkEQeG*iq3O2mUmBqW;ua2a zz+Q&ik>}wOzcLF&SbK*r0AqNE1Ua4oS-TBs0T2V=69wGE2f4t5@6KlL&}mNM+volQ zx3BSrK~WesNl*th>w{&@mx@7vhgFP3D7Zs#UwWN1hd$t*(rT}S1TJN8>H+SV_GiEe zpSYLRl|I4R5PrPqSk2xN3-<`tYRs+o|_+Wy22k|qQ4|tewr+hu=9o!!P9C!jgwh7;NgNJVa2ZaxrQ0RJSa+Dnx{DV&kL~9t6H!@+vlBIo*AY-P1zd zSo1cMLFLojwZWALA0AI+aiZk%h@gGH_OhIgWFT}@;lbMzT*^Rf+*yDd$tXNDk!xyr zv5oeHpA}?{F#u%mGj^3;RlT5>CFbH zjRKrb%Mb^G<0mj+kH=YU7{_UVBk2}aCQmKcx+#;uJRLpwdVya6I5G*qhk0<+g#*wW z1cqZ6nTZ^*(|b=VPbFj~a==dQcVZ%mPdQT7HQv?qMfh3IY+;5NC+F8&3yY!oo6L&l zw;z5@O%(43D#I|QhG%02N+__V4}z zltvXmubzaxA3GHANeQR`0xmXYI5Nr}fQwyJ=%9y~XJ@nG=>t@Okpp&W4>k=DBBKEi zYS>Qxp791mHU&p;@*9i`Ef9u3HGEt5M7&*yI2@ncmga|<&I%jws__*Tn01W>Na&KF zY2<0dF|@qSbL3_Ok%WZM*GkESONi7|Q!+mG!R!3Rs+ zYUjO<>{e9kMRx~TWNPd#Zt8}a6PTi&C*oV}HBH^Zs;?MbE4qU#=v6@D=#WBhwa>gR z_SWd#w6{9_cdPgeyvAO*2ah#&g>3eV#?jtT^ZBhJ@{zy$^8mMk;%_q5|i$5$9O0j64u{)eb_Lu z;2Z9}UZ06z_r84;!+9(F!kg+){*yiMEj(Sa$G?TA+w6^RfxFx6U1)kw5=7)ETg5ER ziJ0g0{SAFaMrK*MZg)xM$fTG`3Q~Grj63tag#LCSisty%Y z7N##qVO<6J8;7jja~~+k&^$jouOlKQ=EM{t9eH;ydBq1ujDHJ?L(l2QLQ& zz8SfLm4?HIG1gM{vg4Rd2k)Ct`^$;_d!(;2`Tng zmJ3ci)#D?)UUmt(`F+l+ed-vM=i5i$SN)wN=S>tJc%V7OhKjTU3zs^1AE+^u^b{e* zgttU0LYyzKoz#zQF$7n`;S7Z?;Vs#bFai+#f-*$RXEeg$PhoHque`{0vLoS31n08^ zz6LnlrW-n7ouXrl=)eH|eP}p4(kJ2URO5hFcEgA2!2p?gb|i|%Vwl{1X`LtmPxIhU zi(#PG7fl+BOqX!AT);uEkDMSoGQ)#E8(QA91ikG&xIBY^UUQFtXLj~FU<;%{EKGiq z9hoKJ%5(uI22VS4KT`9RJ48q#92t`E(Z)`Ck>zEO-9vD^WWz@_fI|}aDwdS!A4E(Z z5Fb^0_#nbi!F)(bj&?x3nd=y1#M=c2)s$bX6DZ(UPG!6&ET}7e7#x~R^dbJ?>`c=^ z24DrREp^mxv20h#3UdW~h4#AoslDqU9Q?O-uaDJ7X)(2vUHh?`5=X^%Lxr#~!9zei zU^jdW#0B>BPgI9*vD+R}bK6XSX8dYj_Pm%^7`YgLBljWIuZyn^?KOwghr4cpW%m>G z_U7VrA(z=A_V}}4c#`wMSv4k+Kj9oTG+W^#oFx%jN}{Ox>~Wq- z)y~Itt&r~!kTrA>E8$r!Xi9rnm!=|!cGTXLtEK!LDCVPKDE&C9AEbK9qX9SQC@G(f zR|OuPaVG1c!w)6gcdOktPfMY4YQzbzy?7HYF zA-Hd5BixV40jEXqH_R9(QrtCSo?P)v=@u&cATB%9FRF#15+szl@M=4A87)VY5!;9}>1a+Tz3=Is#*-*HIvYq$YZ^qLp~&-HmWR zR`dFGq*;;ykI_-$nZh+*>tJ3HMq7z|ZaF3}*ieB`2K^6mruWbmljR-htd*cgh3C;yiO=caPw#db+on4CP=)cnyX9ct)9O$tT7b z(M3gP&rW0^$3FUrk()#&h0n9l)j2c@uivr`8Dr$@FMHV`BQ1`~1);J6%{$EAddNum z8E&t7Ni|-e%E|$u!hSJS+V53w#cbbU2+AxJ0N)Qho!G=bpEH zM~v0pyR-5mhJ5{3Y418>mD@uL+56&$P$Oat`K_Llu zm>4gM<(?)nUy~+j*D+WQ;oCdQ%%mwZ1OVHRoPdhdK4($S8 zYBwahkjRLjZFb(1W(s}vp+7EA@m>i_d5XtS6sw@d{phDcVW`w6G$Wp`7yV7Lk{02a z0q{d+u=PnZlgtvJ8^sc$kYgXk>OZ@}`LFVQ>Zg)I=v*F&sMt=#hFio`1$O_nW?F|U zf~R~nEYL;@G=vcCqP-lbe2~3yt%+9}bZE3|af=U7kDw#Yd;N)}io_Uvg>Rc_P4BzX zGIlpbKS~0%8u2_yV8awO=9JjxTbg*Ij6dAKObOJ-k;SR)?1FNv{**np+|1##?WgdR zB>sd$AYwCt?8P5)Ji4#kboQ0|em)Fl8h^s~*Bf}0{a#wcP#*PYtMmw5kxr}hsCGQ@ U^8@cfz0Lnu9A{WY)Ij6^0JK?|Jpcdz delta 18578 zcmb_^dt6mj`uAG<3U~n&1VaJ@IU*|FZ&d17sUhKHqNb+gH9^HxwA&mjd)0JCYm>Xe z)P|IDY?3gynUzyoT248?OuLv_S|MtwnVQ(yzwfiwK5$Uyoq0c>_mAz``&rLg&;40z zJ?r4PgY6f8(%#QFW2_ne(OB}Wx89li?EXJ+)+nVtpsfbpe9NtWIa&H1iQdm~m6yu&gel zI+S!^jK75W@+V9$DBdx+%_YVrV0_$l6H4azli9tAy%}4u8%Tl4#Z#uw>Q*w1u?IF| zz~816-9NeL)om{`c5DF)%kMRHQo+R3!uAEgB?fy;1)#~Aq>u1D5#MQ3r_Y%`aM+lk zU>2VRD~o1MC>T5Hz(vL$#ry?Zrx(mGZuO-x3;2tHkUXQ{$w1g`eZc&UVZ}3N&l%i$ z<(({i-VVmjPAtA}QgOC<251kCXH1_e6s6p~_10re_Kb_X;a8RvhCap)A00V^+Hca@ z#acu8dpAa^e`jlq&1}9N#}=B;u^jzdHd^V*)~HGB2|V{`jD?}UN?FRLD@WN)XkS-e zb3X=tfi+Z#^WTfNNAXO;Gg^6#eTZi|jR-zkUp5`o3-tTlFZG96xxSL+8X0JtvU2S? zR&I2~x5mnqG5C&Rd3s;8a{>PmIG525VY%9;velOZ7Q+g(qwZ(=S8SPnzu*1RJkFZy z54sls+@e3odJyZxBH9(}U3@+6sL(CYKqn%_q%D-4Rz3a_bsnVO3 zDZSk-+F#|n`4KB6xc0rkFC{$nHhgEY!D=dtM&Dws(ROASOIaIjyL-keR^xb;wOC!_ z&n4+XA4r#t=da2bH-Oe?n#CRoN=FhlcsBEv_?|2zBq_az1k?_)J$eM&M#5a{F~4FD zYLjo0(vgJ02a?T&ge%Y<#&Zr*%?e6K5{5q3F)hn}z`39o8z{=P!oF;A@THL|0d3uhujZ)Z9g+&XEj(ffn$P-}&`=*IF!<5_vc zL{=Vt16X<$VUYvs6|Bsh&B{%{jRJ7uSxh)vZ}&)01Z$Jg)_N$cQN3;9<6-17(EV)i zIV)%bsZ95HaAdI6?__JWHEbj)ya6Yk4a0tf(&pj`D)xRBMLtwV2X3u>?`MO8EY`JG zI15;>0Fn1#Z5{2=AOpenDrHlQARtAoQ0pl@s9arRtF-oPAY~fedOW;!hv7lL3{Ls) zTCzgb{%}*}KI>59D&5D}WuH~rBn#g=o2&`X(IzoJWj(8?Y0m=6U3+whEutUkh$v#} z+2psRR_Y7pHsI_bvcTCxIgY-GV*XWkHusYDEpS!}gBK82R&i8TlX8L@{gB6j zX>E*3Xj$%YUe6r`KDe`vMD>gvuKmo4H3zZQjcwBxvqB1Wt9^88Vve2-h?i)!m)Q;E z%Em!pQ;sZ(&TJ`?lFnEbjnyWySrjG|uhypM1irayZ**54EpiRWT@q7};8D#7LfDbb zvs23I+{W|tpnnnbf|1S5vvm=z*16`N=sC1TSi55!o@*_LEsq|oKESq_AF{t=(aO+{ zu)$T`T1?S+p|$F|34Ho%2{ScatU=bCq^$6A{US8eij`MwOsZ1A!T8o?`1f*a2TI?Z znu&jpr1s@TRdU)l9HB*P9HhUh_0ncAP5!4gD9V3oX$_@iOopx>S%F*}Nv&b)p29b+ zSxOKN(=&wDr)W*I=Yk3J%2p)NT(%yqs^q%Ai*H&%M3Qw6lQYv=DO!K3P{hr>)+cob@lboLsApLL}t|9}6kORWC?ZaKcv-{2m1 zAvSNOOhGYvrIr5MZ_{@>6YH4yA6jd1Ms~}(^)I*%23zY;#s^I*mDk-jmDjN}yzYK# zZEBYp+hG4fPDPJ(y4~N9p6Tlawy5gG%$^*9d#HU*_xk{N8ty=OY8$(dj*K0o*6NoTV-LoT2iRUD6KKGt5W`*I3nva|X!E7M!T-haY=iFUaUBNR?CBYZ60|4cCV zj8W>I3Dev&gdf~iQz{5QZ{5+QCC{%a>Jq2$kFAB>PQ=Os!4@10wy@WsqEGNtSW$xp zRDICBNYx*MC7!bS^-i{i_71BW-@8$io@<h7| zm_11MH_hZi0ryQ{} z_ZC^G?uygQAFevi{DbU5%L0W123-3%Lv{|HW=4+c%EcM4NrN+9)uvIG!uy!}#Tk>u zMln^qwMQGl7~q=u63(nY;FxZ*A8^?GlX6`7N%=*&pysK=)RESq2`T+eBf@BG#2N`k zlF`Q)Zj3PQGK!5k#yiGtW3TbCvCsI-_|5#${MEeFv~SZ#n=Wj+xakv3|J>AWx~6G) z({)WNtkx4dXC03Hw#C2}B`sDZ#w4aDc1#?dI4yC0;)96~CqAC|PU4>HYm;J=5|eI8 z%1s)PG%9JdHGg8Fe_Qff$^S||n0zSt+m!H>!6`W@x2HUwvLrP+wPk8bYTMNIsdG~w zNPReUQEF-G-%~$HJ(~J`YIRy_+P&$k(zm9+m;Rkk^M(6-z8qh!?+#zS?`~hI?^WO1 zzW04cd?(tv8D>VKjQ$x5Gd{~Wkx|=DY1gVYujyWcPaC_%(SN_Mf#IEw&~b0 z*L~4lKX85S`aiB8v3|z-Kd*m!{j(JSdhpPTO_6@c1j$ZIM366S>n#?SrSW%PfAW2 zlr)s|pETCGJSnl@t>k^l2a>;n{#i;Rq5q*NOHx*(#;3MQO-s#8on5E@veb_~{kKjV z--e~HPTv9jbD!aB?8|`u^PvBcz5?GfzDnON=>G@b=?o(y!kRp}gTFOYyyL}v>u+8^ zWc|?fcdox@{S)g;*FRGcR*_KAx}sCXtrfRdDS&xZB+~+)8(o z`-;2K{hPbmU1j|+IsRt1zuV94>-KcJxLIyTH^Xh~rn@Qb_3m|UjN8nOcB96)Ldho)RNvF1R{{+f4d z-mZDG#;JL>=9wD1=INT!nkQ?P);wPGSj{6f{+ix3-D|qmw5z$kCjR(u$A38f{qdv6 zHyoc){dx7l>V4I(RhL#<)lXKhs9s+Em+Ga}PgF0ie!O~N^`ELIRgbS8Q=MOZr}e{> zc2ke_Io9h~r=4r2e^RuxXi?GQMUNHTUvziT$fEqh>cV4%-xVG%{Ic-#!UKgL7QR;a zO5w)B)rEgd`(tWh(#)h8txm*O#;=QC8^0!gb^P=3tK!SzpN+TTXT(pBzc+qFixDjb z#@5DOioF=)GH$0dZS+sOQ`1Io+bfhtW2(NIc}pa!XI07m=F@nj)$Mp!eT}+7jkJo6 z`}mDk>G9dzvchT{J;yz2=2h*iF*y%ddrvqiK;usv$w*p^N2f;f(sM@g?@uTt_+(E6 z&MI$NFPS4-l>P4&5seyVGs)gKo=v1aY-EGE?TN_WM@^#j}Q*r!t ztNK(=EVD^n3awbXQt8ocTZ>jki~ zS#{RuO}G1OCP>^45*Goh;^Xm__DiY<8}f^fPq*g$V(A5eRi<6l_*@p(zrbp@#_IQL zPb`!3e_g8Qx<~a~EA0F{d@ndZLN7)dY*lKUBkLOwEYV$ny2VM5*&9P)0wB}sy z%gg?{F2ffM|tnG@|jUd zeLR(?DEog6+|S!-r};mqUtz3w`4 z&vD{8Z_f2HOtr@Dq;Q}9dLSRKV-!AsU$WOKylaZ+G{zAdguLc{(ZAa z!Y$0gX6Y=Au_!YRHA+6$b=@oBsSV)M>+sVX!0)bur-j0us6?KrOv(++#da+lIO#-- zJ-|^&M}8#pR|+25dOWNq9xO9&4u~}?8&u(T=I0Y@DpN~psvu}+K2?K!eduV;PG}jm*PjK`C(H-D=rI8J(4(fNir$Q#*gl<}(^#W&aXI_gH z+TPujXAy|MVPEdb$0e4#0sTEA89MKP^Dc!27A&w_7RXl*+Vi{dyu|Xr_4+PC!Tdk6 z2+ZG6pp*u#*Dg87x^b1aBonPLi(sNubp|g1PL`ZzWe*+)vhh85mPDe7;!>0~?tAq} z_IO~`ceDGI&TL+Nx3i)LR(v7ID0^#9j%^ND8f90DS6U^U_+D7t{iH|N6p_q*B9gmE zHPafc|4NZO+r_-d5&|yqg=10fM^6Yo91D>=o1&m0ejxh0TNacI1NqiC0nk85Z#h(n;7PleR>hR&J zyT$qpJdx04;vO8BtDe%{zYD$vRG9aHI4QeF0B|t?KD<=;oMecS4B?x~TbhVncR#XN zB#f8N9vDi5dwPsU+e2^U39*spI1B+0?(5TL^C|G5XlKEV9JQT1d;3k$M4o+8Hm&-K zuOYH|NH!b$!miC}e`p`-i#?iSr}cy0ZfT&i8->nbB#npYY^=u$1krCXW!%3lbQX#) z(;B1)^B4jCBtE4z;0eAl3UE0;sbH-0KmGWun3nxibWmuI5@(s-x2Y4>-rkEduy?8^|#>x&}A_n9ICklICPNa_$aO|ld08R9@ z#Mf^MftNNUfcgJO_}7t~5=6;}8}1od?32vSfUl?7u~gi_vxA(4;ExE!cZ#R; zMoRZ9^Q=$5P3BqJts4^%R7-CA9qw_PHx}m7ciEd}^0@6A2xncyDmrMKIhx5j8d*OY2o?jjsiamXsaqYG5`2!F}?n@^^ zm>t?PZU8gg9O4N*>hRm*&~Q^7J}t`cl4}O>y+xen7i%o$Unk}#uUKbH=qLzmb4mSJ zJjpf1E`bQh0d29!5&<#5erYzJT_8P;=9He+UhiKwXPF^+UL$z+3?SAAno{Do)mzm~ zStd3T?`qG47_8iWXpTtX8|Uz@aM~mEm1jrI<(Y#E0x^_u!S+E7kp{MV2(||YVkqGX zIF?et^M$_l2VzA2B>Z6lj${dOqMe0vc~5DP0((aZ@669SXG{3^nr9N3&v3HCR0xGS z70Y}ER|Q;{1i{>38-q>#Uj;t-WBoP;OTdHhZ_FdqCqjnQfe^b#!bPB(o&C_$Rxie8 zQ;c0TV9dQC1B~Oqhn^AfW8BvRTY~s9Azx#?lmcL@4go^!Q^!!G5jSF52m$-pLr}lt z#5~NC#_2x%7Gjap>kwbA>cfBkCh)1V_ZwcOwL}Slp@e|U@Q2G7OoCn(FlWIrK0-%fkAeWy^kD7T}2;tAYmxBrQc zixy#st!U8J<@TzdcvTw_3<8W`fM(%q`YqyHE^(Bp3hX&Q^EYGhN<|*bAa`=qwadQw zG@nUbV1>FmiCL&?l)d{jFKi=x0s9AoLIVW$petMdteK&14x(yR_Q*4QDUY;|oxw@= zytCsh|0qHzs?1sOJ1;ck`xZ@k)uarg4R$s+QeJHwOAbY+P47Gb??AD|nIEsjaaqf9 zN?R%yymxy=f|8j?!$n;|-5^}5jdzq5aOd*`MaijWSFaph|Rgt?QzCh!9@2q_*r{U6cvpwTZp9 zi!uxwpL1QHjGfNOuF7LtqRfw5nx(5BLJ>G4--X{*z(4}-EbFDz@>sEF6Z#dohRM3t zsy<4BtZO;j`zS{_-)yhFK}kRaZNEWTo}B2Fbg*`aVh&8bMb36Ii_;XJl+fNc4FrV)+oz`~nI1H`P#Gq*ZqF}Nk~|HpDpc~M z!Cm`w5s*dFv6H7O?@-W7;p$hswR1;=`c{^d&WY)x&f~HBX`MEX^6q38 z<16CE{lZzBt^SBDue10@4CDXfOzf*(XcSFuyF$2+w}Eqta?~U`D=ZtL;;i6oAEI_| z#aB3Ir>ajIq%NGWTNj}3#UOUS~Cx7EVN;cey*s%o7v$m5I(UW=s4a_VfZ4L0*2X*;ci&w->FV&ZVd z)a#|q)g!{@g9V%tYNZ*E`Pz}B$jV)KYavj+wm?4*_-!=5vUD(AH4}#v2zIZ{DxLY4 z;B~5a70_ok!)FWZ-LkVcIxlWkmx{9SF3)fOF8tOjSHN1ABwW2qz_FVbN?1bW4bTQ2 zc-pVOB;ciiCE6w7Ccq;&oxlYCjt2P4IZ__rrGXuU-w_kh07F?`67;Jj8e(7g^K;bW zoz>O{mhcUBr!8s@BJZIsA^-cNqI23pXe5`zF+@AQ1!Z@yHR$r<7sr1y;i>N0Cb;xjnPM~+^S~Ea*FU*xj!5~ zx!zyP%*GH@ZuiE!uLWKMWx1cs2VCOcT@;ER>EZj>G&YmD@bfhMy0=-~IEo`VZuB%U z?sl=pl_8LVghsosyV^Ad+%+|n5Cz^{xUSWpX)s7Cf$6L4i;zhyL{)a%H`Gi@cW(%# zFOk^=1Z7t!L1S)G`t%e7wXb23NkR#-V~E#l266>m5$)_HU{v?s(oiOGp;% z8sl}rZUB+Pd*_E}ds+Rc#q7S{(!pSG)OK{?4E6b&P<@^qwp}#6WHVJZ&umvSC&}~Z z!=eHhf*Y{q7K3X2qXf~_(}m#f7f27Z<^}QhW91`78L-S2?Qeh+K7}#L z{(y^pgWdKm_=J2NS^v7Y{&lN(4J<)fsv57k=Mg@Sc$)f<&xT;xG@W}IZ0$6pV?Ygn zj}~x=e>>O(9xOPGeMm=6iEod88{#|minkHVx5xr96;v=fq;h5N5Zxk0i9!`*fOa55 z7s$r#(^qycXCHXoJJ6l{jyf*ZF%7Ul1zuBG#U2se!=2K1)S28L)Euq1j$l2wTUg@l z7WQi?0xot7UJd9p<^vw)+a|9Iy-LA@fCEp!#STs4hxPIBjkp5ORcTxNs<8_TeO!TS z2bLmSLkNTnAh#lMWhvr)m{JgkyT>gJETc^qY_cY7bR7Z820{GijUO=oRCnc7^UwA0 z{YKn`kO9L?h5-{A4j|tW1)zmOvi;htk^uBSOl0U6J$P$^dkj%RO9R}9Lmi}!p<$+m z`6szg1~y26{nr>ki8d&K95C5^Dmb7He`y_oDGl(ugTNH%#v@RNKdKIYVhFx}n5ovO zbRvjq#mM_wui-1Cz(fn*5v9Jt%Qz%tmZ9{*S2bP|7R2>wAhU2roumB>hj zFh4n+#Gg>(&EE|it@E7jUxFi#0I5_s-Lk7yYH!-9=J=s?I_iTKj{20_)Q&>%D)5?|EI$V#*@Bho`FxyFN3Ai@GG zLjB?T0YZs#B`*ls0etLfeT$G$F3fVrKuVYR_sErBF8qd6`s+gJq4*T(gpbIb8ZzK5 zFVwFlKw6gsCQvAn#87&fr}PH+q#>d7l~Qw|8lobO)J;@+_(!7bUUvm8ZU{`e8WKM{ zz`Bd91wunX=u-^D{SI9v@nEW{$8RG`V-4^tfsf1ZtMFk8!t+cqi7qQ70R#+99byVe zKm^P+2=qj)8@J@41OiLwildG|&^LzKV?Gco_WTcMsdiraK*f)&9s9^`v2;c45luhQ zIK_L^Y~EeuW1}D{U4?r^nGqY@RMLe^iy`pu0>20yp`c-A+DATA6S`*_l_;Y5IJvwL zv2!u0(8PW>R}|2E?C)a2t?n?Gk@__BEw?*;q$af7hxHQ_I}^n>Tyb3?y5sDLAE^Ud z1~7BFdYhQnNtUA@$Io#t+dI+ChuMwystLV^fju9JcYhUhz6)TbtYdMk;4M?xDEb`t zA+HZR5_6_KeyqIX6222n{XCOfcmrTbwz$5-9={LUwbp`u_f+BFzoBQeSWWRfneSF{i-iR3RTbifd?Bf8D*&0 zP=WX&XVrc+644jP$3_^@7oiB+FEf}`Ea^eG8VYB6AfKY|Gsr~ImmYy|ghRcIKc5sb z;U+NGjyizu<*-)9BoDbvAcIs`6MH;$ zlXIs>q{wa+;YR8MC#CiZ>L%w-k3iZWQe-vRZG2|$KsP__e11^PR>q1@%xZ% zbTcP24Q|@J41`NU{ICb`m$57OmME*fKhV?%T(PcrnQ1a~DJuNf{y(0e-Bt(?6@LiuECzJKd&c<)lS2@-{ z{3O#E|E;<-j6ZLmJdSif-ELK*+H?b>EwZ=PsI_{SyUV<2Z#|(N?VN^ec%#^3w058X z)>@q+*vBzVFP3oPlWCNTA=Y$f?@5#{X$tkCefpF-0b zj=ByIAUF{)o(=Yg|6sq=O-sFnXpIe_mCX+cl=INuBY+2woA@@U{v143w$~GacwtlR zsP0;V_?6WPF+`S}X|IJF3rvwtsA7jq_19qC6{P7h1mB%RKSEY)A`RL^0w3RMWc{X+ z4dKY#60Z5{;64eDGW~3bawLdg%&$W**(m<5fMeYhCv$^N=9mcI`cZ+0Z>VR8c5@JJ z4y%KM0KutW-yG}@d+(}#JEMozjDF#3JZ<;up~YqFL=hl(D#vMEN39O(8|{%o3rhiR zPO}&E&=R_1q$hr!ph9UFcUYK0=0*CZs z9AgZ*yy5Z&eyav@3QBhZ$Jwyfc;))oFQ6W(rFfn#>SvWs(K|*X&L4K>?=U`7cwguC z_l$cv|JK>M)3|^?rxiCvoA(&LFkFBg`j_!bBf4+Yn~7VGHbRH^Mjh_h*Tyi>Ma6l~ z`{ezZJ>;;F6)TFw;%cp3a1<^uR~$Bm$;(2!`mmAYm-m>mOhWSx7L#R&5qt2RN|%?7 zJ`B!q)ji)Rf!{gUuOU>xj0_b~Tx)o-s6Q$Can}QFWiw{~pKpv=K}BI;@8Cqp>5Y#V zBchwb9}bE!eW=SlZ_hbmEN|nf79)@-VZJD+XezN9eIhu6R%y5W*2r!nG%99jFS^8G zNpOX;42FtdAv=q|HBPAf8E4M-M!70~dxIeNvHSUV@FRoHJ-`*42&da|BR3*^in?2S zh<;OT7hdEY?XABUkI5gVJN?cXnUVasQ>2>V1w6tjnP|SJ@_#ygrlR7-d)iOTH+%AI z=iT|{_euVGCy|nP#s*1g?EfO)h?0Rp2V$`384=^95j{7+cfA`F2&c>>d^`PFGqzxg z0CWgWK!te>=otJatKwT)6$Bb2IAR!HfPU!(o__Hybw&(T*{hy46X<6-4-h=0yZjh5+G`m`u^*+ZT)6Z-$B)I_KH_QnV;b3sU)F4E~A%;QBkr>(XUqvp(0-{!(+qH_DID9h9qa=TMpXfhKP7@uxK4 z92#+59tO^n?5%6T!n5}0Yt78=_d>SqV%2IXMn8(te-WC!PJD~Mwh{da;#>U9&7xi} zm`<-3!fqJ^C;rp%us8eg2)j>O_&^@9=+U%rJQ2>LX^pPuF<; + + /** The character index to initially select with the cursor when editing is activated. Defaults to `0`. */ + initialEditIndex?: number; + + /** + * A function or {@link VNode} which renders the input's value when editing is not active. If defined, the rendered + * inactive value replaces all rendered child components when editing is not active. + */ + renderInactiveValue?: VNode | ((value: string) => string | VNode); + + /** CSS class(es) to apply to the root of the component. */ + class?: string | SubscribableSet | ToggleableClassNameRecord; +} + +/** + * An input with a scrolling cursor that allows users to select an arbitrary string. The composite value bound to the + * input is derived from the in-order concatenation of the values of all child `CharInputSlot` components. + */ +export class CharInput extends DisplayComponent { + private static readonly LAST_NON_EMPTY_SLOT_INDEX = (lastIndex: number, value: string, index: number): number => value !== '' ? index : lastIndex; + + private readonly inputRef = FSComponent.createRef>>(); + + private readonly value = Subject.create(''); + + private readonly slots: CharInputSlot[] = []; + + // eslint-disable-next-line jsdoc/require-returns + /** The index of the character position currently selected by this input's cursor. */ + public get cursorPosition(): Subscribable { + return this.inputRef.instance.cursorPosition; + } + + // eslint-disable-next-line jsdoc/require-returns + /** Whether editing is active for this input. */ + public get isEditingActive(): Subscribable { + return this.inputRef.instance.isEditingActive; + } + + // eslint-disable-next-line jsdoc/require-returns + /** Whether this input's cursor selection mode is per-slot. */ + public get isSelectionPerSlot(): Subscribable { + return this.inputRef.instance.isSelectionPerSlot; + } + + private isInit = false; + + private valuePipeOut?: Subscription; + + /** @inheritdoc */ + public onAfterRender(thisNode: VNode): void { + FSComponent.visitNodes(thisNode, node => { + if (node.instance instanceof CharInputSlot) { + this.slots.push(node.instance); + return true; + } + + return false; + }); + + this.valuePipeOut = this.value.pipe(this.props.value); + + MappedSubject.create( + values => values.reduce(CharInput.LAST_NON_EMPTY_SLOT_INDEX, -1), + ...this.slots.map(slot => slot.value) + ).sub(this.updateAllowEmptySlotValues.bind(this), true); + + this.isInit = true; + } + + /** + * Updates whether each of this input's slots should allow empty values. + * @param lastNonEmptySlotIndex The index of the last slot with a non-empty value. + */ + private updateAllowEmptySlotValues(lastNonEmptySlotIndex: number): void { + for (let i = 0; i < this.slots.length; i++) { + this.slots[i].setAllowEmptyValue(i >= lastNonEmptySlotIndex); + } + } + + /** + * Checks whether this input is initialized. + * @returns Whether this input is initialized. + */ + public isInitialized(): boolean { + return this.isInit; + } + + /** + * Sets the composite value of this input. As part of the operation, all of this input's child slots will have their + * values set according to this input's value digitizer, and all slot characters will be set to non-null + * representations of their slot's value, if possible. The composite value of this input after the operation is + * complete may differ from the requested value depending on whether the requested value can be accurately + * represented by this input. + * @param value The new composite value. + * @returns The composite value of this input after the operation is complete. + * @throws Error if this input is not initialized. + */ + public setValue(value: string): string { + if (!this.isInitialized()) { + throw new Error('CharInput: attempted to manipulate input before it was initialized'); + } + + return this.inputRef.instance.setValue(value); + } + + /** + * Activates editing for this input. + * @param isSelectionPerSlot Whether cursor selection should be initialized to per-slot mode. If `false`, cursor + * selection will be initialized to per-character mode instead. + * @throws Error if this input is not initialized. + */ + public activateEditing(isSelectionPerSlot: boolean): void { + if (!this.isInitialized()) { + throw new Error('CharInput: attempted to manipulate input before it was initialized'); + } + + this.inputRef.instance.activateEditing(isSelectionPerSlot); + } + + /** + * Deactivates editing for this input. + * @throws Error if this input is not initialized. + */ + public deactivateEditing(): void { + if (!this.isInitialized()) { + throw new Error('CharInput: attempted to manipulate input before it was initialized'); + } + + this.inputRef.instance.deactivateEditing(); + } + + /** + * Moves the cursor. + * @param direction The direction in which to move (`1` = to the right, `-1` = to the left). + * @param forceSelectionPerSlot Whether to force cursor selection to per slot mode. + * @throws Error if this input is not initialized. + */ + public moveCursor(direction: 1 | -1, forceSelectionPerSlot: boolean): void { + if (!this.isInitialized()) { + throw new Error('CharInput: attempted to manipulate input before it was initialized'); + } + + // Do not allow the cursor to move to the right of any slots that have null or empty string values. + if (direction === 1) { + // All slots are guaranteed to only have one character, so character position is the same as slot index. + const cursorPosition = this.inputRef.instance.cursorPosition.get(); + for (let i = cursorPosition; i >= 0; i--) { + if (!this.slots[i].value.get()) { + return; + } + } + } + + this.inputRef.instance.moveCursor(direction, forceSelectionPerSlot); + } + + /** + * Places the cursor at a specific character position. + * @param index The index of the character position at which to place the cursor. + * @param forceSelectionPerSlot Whether to force cursor selection to per slot mode. + * @throws Error if this input is not initialized. + * @throws RangeError if `index` does not point to a valid character position. + */ + public placeCursor(index: number, forceSelectionPerSlot: boolean): void { + if (!this.isInitialized()) { + throw new Error('CharInput: attempted to manipulate input before it was initialized'); + } + + this.inputRef.instance.placeCursor(index, forceSelectionPerSlot); + } + + /** + * Increments or decrements the value of the slot currently selected by the cursor. If editing is not active, then it + * will be activated instead of changing any slot value. If cursor selection is in per-character mode, it will be + * forced to per-slot mode. If the cursor is past the last slot, then this method does nothing. + * @param direction The direction in which to change the slot value (`1` = increment, `-1` = decrement). + * @param eraseCharsToRightOnEdit Whether to erase (set to `null`) all characters to the right of the edited + * character. Defaults to `false`. + * @throws Error if this input is not initialized. + */ + public changeSlotValue(direction: 1 | -1, eraseCharsToRightOnEdit = false): void { + if (!this.isInitialized()) { + throw new Error('CharInput: attempted to manipulate input before it was initialized'); + } + + const wasChanged = this.inputRef.instance.changeSlotValue(direction); + + if (wasChanged && eraseCharsToRightOnEdit) { + const cursorPosition = this.inputRef.instance.cursorPosition.get(); + + for (let i = this.slots.length - 1; i > cursorPosition; i--) { + this.slots[i].setChar(null); + } + } + } + + /** + * Sets the value of the slot character currently selected by the cursor. If editing is not active, then it will be + * activated before setting the value. If the cursor is past the last slot, then this method does nothing. + * @param value The value to set. + * @param eraseCharsToRightOnEdit Whether to erase (set to `null`) all characters to the right of the edited + * character. Defaults to `false`. + * @throws Error if this input is not initialized. + */ + public setSlotCharacterValue(value: string, eraseCharsToRightOnEdit = false): void { + if (!this.isInitialized()) { + throw new Error('CharInput: attempted to manipulate input before it was initialized'); + } + + const wasChanged = this.inputRef.instance.setSlotCharacterValue(value); + + if (wasChanged && eraseCharsToRightOnEdit) { + // All slots are guaranteed to only have one character, so character position is the same as slot index. + const cursorPosition = this.inputRef.instance.cursorPosition.get(); + // Setting the slot character value will move the cursor one position to the right, so we need to empty all slots + // from the end to the current cursor position, inclusive. + for (let i = this.slots.length - 1; i >= cursorPosition; i--) { + this.slots[i].setChar(null); + } + } + } + + /** + * Removes the character at the cursor's current position and shifts the cursor one position to the left after the + * character is removed. + * @throws Error if this input is not initialized. + */ + public backspace(): void { + if (!this.isInitialized()) { + throw new Error('CharInput: attempted to manipulate input before it was initialized'); + } + + this.inputRef.instance.backspace(); + } + + /** + * Populates all of this input's character positions with non-empty values, if possible, using this input's value + * digitizer function and the current composite value as a template. + */ + public populateCharsFromValue(): void { + this.inputRef.getOrDefault()?.populateCharsFromValue(); + } + + /** + * Refreshes this input, updating the size and position of the cursor. + */ + public refresh(): void { + this.inputRef.getOrDefault()?.refresh(); + } + + /** + * Parses a composite value from this input's individual slots. + * @returns The composite value represented by this input's individual slots. + */ + private parseValue(): string { + return this.slots.reduce((prev, curr) => prev + curr.value.get(), ''); + } + + /** + * Digitizes a composite value into individual slot values to assign to this input's slots. + * @param value The value to digitize. + */ + private digitizeValue(value: string): void { + for (let i = 0; i < this.slots.length; i++) { + const char = value[i]; + if (char) { + this.slots[i].setChar(char); + } else { + this.slots[i].setChar(null); + } + } + } + + /** @inheritdoc */ + public render(): VNode { + return ( + + {this.props.children} + + ); + } + + /** @inheritdoc */ + public destroy(): void { + this.inputRef.getOrDefault()?.destroy(); + + this.valuePipeOut?.destroy(); + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CharInput/CharInputSlot.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CharInput/CharInputSlot.tsx new file mode 100644 index 000000000..a0e178a34 --- /dev/null +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CharInput/CharInputSlot.tsx @@ -0,0 +1,267 @@ +import { + ComponentProps, DisplayComponent, FSComponent, MutableSubscribable, SetSubject, Subscribable, + SubscribableSet, SubscribableUtils, Subscription, ToggleableClassNameRecord, VNode, +} from '@microsoft/msfs-sdk'; + +import { GenericCursorInputSlot } from '../CursorInput/CursorInputSlot'; + +/** + * Component props for CharInputSlot. + */ +export interface CharInputSlotProps extends ComponentProps { + /** + * An array of valid character values for the slot. The order of characters in the array determines the order in + * which the slot will cycle through characters when incrementing/decrementing its value. + */ + charArray: readonly string[]; + + /** + * Whether the slot should wrap from the last valid character to the first valid character and vice-versa when + * incrementing/decrementing its value. + */ + wrap: boolean | Subscribable; + + /** The default character value for the slot when the character value is `null`. */ + defaultCharValue: string | Subscribable; + + /** + * A function which renders slot characters into string. If not defined, non-null characters will be rendered as-is, + * and null characters will be rendered according to the default value assigned to that character. + */ + renderChar?: (character: string | null, index: number) => string; + + /** CSS class(es) to apply to the slot's root element. */ + class?: string | SubscribableSet | ToggleableClassNameRecord; +} + +/** + * A cursor input slot which allows the user to select a single arbitrary character. + */ +export class CharInputSlot extends DisplayComponent { + private static readonly RESERVED_CSS_CLASSES = ['char-input-slot']; + + private readonly slotRef = FSComponent.createRef>(); + + private readonly defaultCharValue = SubscribableUtils.toSubscribable(this.props.defaultCharValue, true); + + private readonly parseValue = (characters: readonly (string | null)[]): string => { + return characters[0] ?? ''; + }; + + private readonly digitizeValue = (value: string, setCharacters: readonly ((char: string | null) => void)[]): void => { + if (value === '' || !this.props.charArray.includes(value)) { + setCharacters[0](null); + } else { + setCharacters[0](value); + } + }; + + private readonly renderChar = this.props.renderChar ?? ( + (character: string | null): string => { + const characterToRender = character === null ? this.defaultCharValue.get() : character; + return characterToRender === '' + ? '_' + : characterToRender === '0' + ? '0̸' + : characterToRender; + } + ); + + private readonly wrap = SubscribableUtils.toSubscribable(this.props.wrap, true); + + // eslint-disable-next-line jsdoc/require-returns + /** The value bound to this slot. */ + public get value(): Subscribable { + return this.slotRef.instance.value; + } + + private allowEmptyValue = true; + + private readonly subscriptions: Subscription[] = []; + + /** @inheritdoc */ + public onAfterRender(): void { + this.subscriptions.push( + this.defaultCharValue.sub(() => { + this.slotRef.instance.refreshFromChars(); + }, true) + ); + } + + /** + * Sets whether this slot should allow its value to be set to the empty string. Disallowing empty string values will + * not cause this slot's current value to change, even if the current value is the empty string. + * @param allow Whether this slot should allow its value to be set to the empty string. + */ + public setAllowEmptyValue(allow: boolean): void { + this.allowEmptyValue = allow; + } + + /** + * Sets the value of this slot. As part of the operation, this slot's character will be set to a non-null + * representation of the new value, if possible. The value of this slot after the operation is complete may differ + * from the requested value depending on whether the requested value can be accurately represented by this slot. + * @param value The new value. + * @returns The value of this slot after the operation is complete. + */ + public setValue(value: string): string { + return this.slotRef.instance.setValue(value); + } + + /** + * Increments this slot's value. + * @returns Whether the increment operation was accepted. + */ + public incrementValue(): boolean { + return this.slotRef.instance.incrementValue(); + } + + /** + * Decrements this slot's value. + * @returns Whether the decrement operation was accepted. + */ + public decrementValue(): boolean { + return this.slotRef.instance.decrementValue(); + } + + /** + * Sets the value of this slot's character. + * @param char The value to set. + * @param force Whether to force the character to accept a value that would normally be invalid. Defaults to `false`. + * @returns Whether the operation was accepted. + */ + public setChar(char: string | null, force?: boolean): boolean { + return this.slotRef.instance.setChar(0, char, force); + } + + /** + * Changes this slot's value in a specified direction. + * @param direction The direction in which to change the value. + * @param value This slot's current value. + * @param setValue A function which sets this slot's value. + * @returns Whether the value was successfully changed. + */ + private changeValue(direction: 1 | -1, value: string, setValue: (value: string) => void): boolean { + if (this.props.charArray.length === 0) { + return false; + } + + let currentIndex = this.props.charArray.indexOf(value); + + if (currentIndex < 0) { + currentIndex = direction === 1 ? -1 : this.props.charArray.length; + } + + let newIndex: number | undefined = undefined; + + for (let i = 0; i < this.props.charArray.length; i++) { + currentIndex += direction; + + if (currentIndex < 0 || currentIndex >= this.props.charArray.length) { + if (this.wrap.get()) { + if (currentIndex < 0) { + currentIndex = this.props.charArray.length - 1; + } else { + currentIndex = 0; + } + } else { + break; + } + } + + if (this.allowEmptyValue || this.props.charArray[currentIndex] !== '') { + newIndex = currentIndex; + break; + } + } + + if (newIndex !== undefined) { + setValue(this.props.charArray[newIndex]); + return true; + } else { + return false; + } + } + + /** + * Sets the value of one of this slot's characters. + * @param characters An array of characters. + * @param index The index of the character to set. + * @param charToSet The value to set. + * @param force Whether to force the character to accept a value that would normally be invalid. Defaults to `false`. + * @returns Whether the operation was accepted. + */ + private _setChar(characters: readonly MutableSubscribable[], index: number, charToSet: string | null, force?: boolean): boolean { + if (this.canSetChar(index, charToSet, force)) { + characters[index].set(charToSet); + return true; + } else { + return false; + } + } + + /** + * Checks whether one of this slot's characters can be set to a given value. + * @param index The index of the character to set. + * @param character The value to set. + * @param force Whether the character should accept a value that would normally be invalid. + * @returns Whether the specified character can be set to the specified value. + */ + private canSetChar(index: number, character: string | null, force?: boolean): boolean { + if (character === null || force) { + return true; + } + + return this.props.charArray.includes(character) && (this.allowEmptyValue || character !== ''); + } + + /** @inheritdoc */ + public render(): VNode { + let cssClass: string | SetSubject; + + if (typeof this.props.class === 'object') { + cssClass = SetSubject.create(); + cssClass.add('char-input-slot'); + + const sub = FSComponent.bindCssClassSet(cssClass, this.props.class, CharInputSlot.RESERVED_CSS_CLASSES); + if (Array.isArray(sub)) { + this.subscriptions.push(...sub); + } else { + this.subscriptions.push(sub); + } + } else { + cssClass = 'char-input-slot'; + + if (this.props.class !== undefined && this.props.class.length > 0) { + cssClass += ' ' + FSComponent.parseCssClassesFromString(this.props.class, classToAdd => !CharInputSlot.RESERVED_CSS_CLASSES.includes(classToAdd)).join(' '); + } + } + + return ( + + ref={this.slotRef} + allowBackfill={false} + characterCount={1} + parseValue={this.parseValue} + digitizeValue={this.digitizeValue} + renderChar={this.renderChar} + incrementValue={this.changeValue.bind(this, 1)} + decrementValue={this.changeValue.bind(this, -1)} + setChar={this._setChar.bind(this)} + canSetChar={(characters, index, charToSet, force): boolean => this.canSetChar(index, charToSet, force)} + class={cssClass} + /> + ); + } + + /** @inheritdoc */ + public destroy(): void { + this.slotRef.getOrDefault()?.destroy(); + + for (const sub of this.subscriptions) { + sub.destroy(); + } + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CharInput/index.ts b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CharInput/index.ts new file mode 100644 index 000000000..2918ced69 --- /dev/null +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CharInput/index.ts @@ -0,0 +1,2 @@ +export * from './CharInput'; +export * from './CharInputSlot'; \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInput.css b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInput.css index 703f8074a..4cb07b0a6 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInput.css +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInput.css @@ -18,6 +18,7 @@ html { } .cursor-input { + position: relative; background: var(--cursor-input-background); border-radius: var(--cursor-input-border-radius); line-height: var(--cursor-input-slots-line-height); diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInput.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInput.tsx index 782e86a6f..1a65ce789 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInput.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInput.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { ComponentProps, DebounceTimer, DisplayComponent, FSComponent, MappedSubject, MappedSubscribable, MathUtils, MutableSubscribable, ObjectSubject, - SetSubject, Subject, Subscribable, SubscribableSet, SubscribableType, SubscribableUtils, Subscription, VNode + SetSubject, Subject, Subscribable, SubscribableSet, SubscribableType, SubscribableUtils, Subscription, ToggleableClassNameRecord, VNode } from '@microsoft/msfs-sdk'; import { CursorInputSlot } from './CursorInputSlot'; @@ -73,7 +73,7 @@ export interface CursorInputProps> extends Co renderInactiveValue?: VNode | ((value: SubscribableType) => string | VNode); /** CSS class(es) to apply to the component's root element. */ - class?: string | SubscribableSet; + class?: string | SubscribableSet | ToggleableClassNameRecord; } /** @@ -170,7 +170,7 @@ export class CursorInput> extends DisplayComp private isInit = false; - private cssClassSub?: Subscription; + private cssClassSub?: Subscription | Subscription[]; private valuePipeOut?: Subscription; private inactiveValueSub?: Subscription; @@ -587,16 +587,17 @@ export class CursorInput> extends DisplayComp * will be activated instead of changing any slot value. If cursor selection is in per-character mode, it will be * forced to per-slot mode. If the cursor is past the last slot, this method does nothing. * @param direction The direction in which to change the slot value (`1` = increment, `-1` = decrement). + * @returns Whether the value of the slot was changed. * @throws Error if this input is not initialized. */ - public changeSlotValue(direction: 1 | -1): void { + public changeSlotValue(direction: 1 | -1): boolean { if (!this.isInitialized()) { throw new Error('CursorInput: attempted to manipulate input before it was initialized'); } if (!this._isEditingActive.get()) { this.activateEditing(true); - return; + return false; } this._isSelectionPerSlot.set(true); @@ -606,14 +607,14 @@ export class CursorInput> extends DisplayComp const cursorPosition = this._cursorPosition.get(); if (cursorPosition >= this.charPositions.length) { - return; + return false; } const slot = this.charPositions[cursorPosition].slot; if (direction === 1) { - slot.incrementValue(); + return slot.incrementValue(); } else { - slot.decrementValue(); + return slot.decrementValue(); } } @@ -1006,7 +1007,16 @@ export class CursorInput> extends DisplayComp this.cleanUpRenderedInactiveValue(); - this.cssClassSub?.destroy(); + if (this.cssClassSub) { + if (Array.isArray(this.cssClassSub)) { + for (const sub of this.cssClassSub) { + sub.destroy(); + } + } else { + this.cssClassSub.destroy(); + } + } + this.valuePipeOut?.destroy(); this.inactiveValueSub?.destroy(); diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInputSlot.css b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInputSlot.css index 52052b3e3..52d5debc9 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInputSlot.css +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInputSlot.css @@ -4,6 +4,7 @@ justify-content: flex-start; align-items: baseline; color: var(--g3000-color-cyan); + white-space: pre; } @keyframes cursor-input-slot-blink { diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInputSlot.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInputSlot.tsx index 287a437ba..e55191b12 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInputSlot.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/CursorInput/CursorInputSlot.tsx @@ -1,6 +1,6 @@ import { ComponentProps, DisplayComponent, FSComponent, MappedSubject, MutableSubscribable, SetSubject, Subject, - Subscribable, SubscribableSet, SubscribableUtils, Subscription, VNode + Subscribable, SubscribableSet, SubscribableUtils, Subscription, ToggleableClassNameRecord, VNode } from '@microsoft/msfs-sdk'; import './CursorInputSlot.css'; @@ -244,7 +244,7 @@ export interface GenericCursorInputSlotProps extends ComponentProps { canSetChar: (characters: readonly (string | null)[], index: number, charToSet: string | null, force: boolean) => boolean; /** CSS class(es) to apply to the component's root element. */ - class?: string | SubscribableSet; + class?: string | SubscribableSet | ToggleableClassNameRecord; } /** @@ -290,7 +290,7 @@ export class GenericCursorInputSlot protected readonly setValueFunc = this.setValue.bind(this); - private cssClassSub?: Subscription; + private cssClassSub?: Subscription | Subscription[]; /** @inheritdoc */ public onAfterRender(): void { @@ -482,6 +482,16 @@ export class GenericCursorInputSlot /** @inheritdoc */ public destroy(): void { - this.cssClassSub?.destroy(); + if (this.cssClassSub) { + if (Array.isArray(this.cssClassSub)) { + for (const sub of this.cssClassSub) { + sub.destroy(); + } + } else { + this.cssClassSub.destroy(); + } + } + + super.destroy(); } } \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/Keyboard.css b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/Keyboard.css new file mode 100644 index 000000000..71d62bc1f --- /dev/null +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/Keyboard.css @@ -0,0 +1,191 @@ +.keyboard-container { + position: relative; +} + +.gtc-horizontal .keyboard-container { + width: 867px; + height: 675px; + font-size: 55px; + + --keyboard-numpad-top: 138px; + + --keyboard-row-gap: 15px; + --keyboard-column-gap: 24px; + + --keyboard-numpad-group-left: 222px; + + --keyboard-numpad-space-key-right: 3px; + + --keyboard-button-width: 124px; + --keyboard-button-wide-width: 198px; + --keyboard-button-height: 123px; + + --keyboard-space-key-font-size: 45px; + + --keyboard-backspace-font-size: 32px; + --keyboard-backspace-icon-left: 45px; + --keyboard-backspace-icon-top: 68px; + --keyboard-backspace-icon-width: 95px; + --keyboard-backspace-label-top: 25px; + + --keyboard-find-font-size: 34px; + --keyboard-find-icon-left: 8px; + --keyboard-find-icon-top: 10px; + --keyboard-find-icon-width: 90px; + --keyboard-find-label-top: 75px; +} + +.gtc-vertical .keyboard-container { + width: 467px; + height: 362px; + font-size: 28px; + + --keyboard-numpad-top: 74px; + + --keyboard-row-gap: 8px; + --keyboard-column-gap: 13px; + + --keyboard-numpad-group-left: 120px; + + --keyboard-numpad-space-key-right: 0px; + + --keyboard-button-width: 67px; + --keyboard-button-wide-width: 107px; + --keyboard-button-height: 66px; + + --keyboard-space-key-font-size: 25px; + + --keyboard-backspace-font-size: 17px; + --keyboard-backspace-icon-left: 23px; + --keyboard-backspace-icon-top: 36px; + --keyboard-backspace-icon-width: 50px; + --keyboard-backspace-label-top: 13px; + + --keyboard-find-font-size: 18px; + --keyboard-find-icon-left: 3px; + --keyboard-find-icon-top: 4px; + --keyboard-find-icon-width: 50px; + --keyboard-find-label-top: 41px; +} + +.keyboard-container-mode { + position: absolute; + left: 0px; + width: 100%; + display: flex; + flex-flow: column nowrap; + pointer-events: none; +} + +.keyboard-alpha { + top: 0px; +} + +.keyboard-numpad { + top: var(--keyboard-numpad-top); +} + +.keyboard-container-mode .touch-button { + pointer-events: auto; +} + +.keyboard-alpha-key, +.keyboard-numpad-key { + opacity: 1; + transition: opacity 250ms; +} + +.keyboard-container-alpha .keyboard-container-mode .keyboard-numpad-key, +.keyboard-container-numpad .keyboard-container-mode .keyboard-alpha-key { + opacity: 0; + pointer-events: none; +} + +.keyboard-numpad .keyboard-space-key { + position: absolute; + right: var(--keyboard-numpad-space-key-right); + bottom: 0px; +} + +.keyboard-row { + display: flex; + flex-flow: row nowrap; + margin-bottom: var(--keyboard-row-gap); +} + +.keyboard-row>.touch-button+.touch-button { + margin-left: var(--keyboard-column-gap); +} + +.keyboard-numpad-column { + display: flex; + flex-flow: column nowrap; +} + +.keyboard-numpad-column>.touch-button+.touch-button { + margin-top: var(--keyboard-row-gap); +} + +.keyboard-numpad-group { + position: absolute; + left: var(--keyboard-numpad-group-left); + top: 0px; +} + +.keyboard-numpad-group .keyboard-row { + width: 100%; + justify-content: space-around; +} + +.keyboard-container .touch-button { + width: var(--keyboard-button-width); + height: var(--keyboard-button-height); +} + +.keyboard-container .touch-button.keyboard-button-wide { + width: var(--keyboard-button-wide-width); +} + +.keyboard-space-key { + font-size: var(--keyboard-space-key-font-size); +} + +.keyboard-backspace, +.keyboard-find { + display: flex; + flex-flow: column nowrap; + justify-content: center; + align-items: center; +} + +.keyboard-backspace { + font-size: var(--keyboard-backspace-font-size); +} + +.keyboard-backspace-icon { + position: absolute; + left: var(--keyboard-backspace-icon-left); + top: var(--keyboard-backspace-icon-top); + width: var(--keyboard-backspace-icon-width); +} + +.keyboard-backspace-label { + position: absolute; + top: var(--keyboard-backspace-label-top); +} + +.keyboard-find { + font-size: var(--keyboard-find-font-size); +} + +.keyboard-find-icon { + position: absolute; + left: var(--keyboard-find-icon-left); + top: var(--keyboard-find-icon-top); + width: var(--keyboard-find-icon-width); +} + +.keyboard-find-label { + position: absolute; + top: var(--keyboard-find-label-top); +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/Keyboard.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/Keyboard.tsx new file mode 100644 index 000000000..2b9f975b0 --- /dev/null +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/Keyboard.tsx @@ -0,0 +1,239 @@ +import { + ComponentProps, DisplayComponent, FSComponent, SetSubject, Subject, SubscribableSet, Subscription, + ToggleableClassNameRecord, VNode +} from '@microsoft/msfs-sdk'; + +import { TouchButton } from '@microsoft/msfs-garminsdk'; + +import './Keyboard.css'; + +/** + * Component props for Keyboard. + */ +export interface KeyboardProps extends ComponentProps { + /** + * Whether to show the "Find" button. If `true`, then the "Find" button will replace the space button while the + * keyboard is displaying letters, and the space button will instead be shown while the keyboard is displaying + * numerals. + */ + showFindButton: boolean; + + /** Whether the space button is enabled. */ + isSpaceButtonEnabled: boolean; + + /** A callback function which is called when a character key button is pressed. */ + onKeyPressed?: (char: string) => void; + + /** A callback function which is called when the backspace button is pressed. */ + onBackspacePressed?: () => void; + + /** A callback function which is called when the "Find" button is pressed. */ + onFindPressed?: () => void; + + /** CSS class(es) to apply to the number pad's root element. */ + class?: string | SubscribableSet | ToggleableClassNameRecord; +} + +/** + * A keyboard with buttons for all alphanumeric characters and the space character, a backspace button, and an optional + * "Find" button. The display of letters and numerals is mutually exclusive, and the keyboard can be toggled between + * the two states. The letters are ordered alphabetically. + */ +export class Keyboard extends DisplayComponent { + private static readonly RESERVED_CSS_CLASSES = [ + 'keyboard-container', + 'keyboard-container-alpha', + 'keyboard-container-numpad' + ]; + + private thisNode?: VNode; + + private readonly rootRef = FSComponent.createRef(); + + private readonly rootCssClass = SetSubject.create(['keyboard-container']); + + private readonly showNumpad = Subject.create(false); + + private cssClassSub?: Subscription | Subscription[]; + + /** @inheritDoc */ + public onAfterRender(thisNode: VNode): void { + this.thisNode = thisNode; + + this.showNumpad.sub(showNumpad => { + this.rootCssClass.toggle('keyboard-container-alpha', !showNumpad); + this.rootCssClass.toggle('keyboard-container-numpad', showNumpad); + }, true); + } + + /** + * Sets whether the keyboard shows the numpad keys instead of the alphabet keys. + * @param show Whether to show the numpad keys. + */ + public setShowNumpad(show: boolean): void { + this.showNumpad.set(show); + } + + /** + * Responds to when this keyboard's mode button is pressed. + */ + private onModePressed(): void { + this.showNumpad.set(!this.showNumpad.get()); + } + + /** + * Responds to when one of this keyboard's character keys is pressed. + * @param char The character of the key that was pressed. + */ + private onKeyPressed(char: string): void { + this.props.onKeyPressed && this.props.onKeyPressed(char); + } + + /** + * Responds to when this keyboard's backspace button is pressed. + */ + private onBackspacePressed(): void { + this.props.onBackspacePressed && this.props.onBackspacePressed(); + } + + /** + * Responds to when this keyboard's find button is pressed. + */ + private onFindPressed(): void { + this.props.onFindPressed && this.props.onFindPressed(); + } + + /** @inheritdoc */ + public render(): VNode { + if (typeof this.props.class === 'object') { + this.cssClassSub = FSComponent.bindCssClassSet(this.rootCssClass, this.props.class, Keyboard.RESERVED_CSS_CLASSES); + } else if (this.props.class) { + for (const classToAdd of FSComponent.parseCssClassesFromString(this.props.class, classToFilter => !Keyboard.RESERVED_CSS_CLASSES.includes(classToFilter))) { + this.rootCssClass.add(classToAdd); + } + } + + const renderAlphaKey = (char: string): VNode => this.renderKey('keyboard-alpha-key', char); + const renderNumpadKey = (char: string): VNode => this.renderKey('keyboard-numpad-key', char); + + return ( +
+
+
+ {['A', 'B'].map(renderAlphaKey)} + { + this.props.showFindButton + ? ( + + +
Find
+
+ ) : ( + + ) + } + x ? 'ABC...' : '123...')} + onPressed={this.onModePressed.bind(this)} + /> + +
Backspace
+ +
+
+
+ {['C', 'D', 'E', 'F', 'G', 'H'].map(renderAlphaKey)} +
+
+ {['I', 'J', 'K', 'L', 'M', 'N'].map(renderAlphaKey)} +
+
+ {['O', 'P', 'Q', 'R', 'S', 'T'].map(renderAlphaKey)} +
+
+ {['U', 'V', 'W', 'X', 'Y', 'Z'].map(renderAlphaKey)} +
+
+
+
+ {['N', 'S', 'E', 'W'].map(renderNumpadKey)} +
+
+
+ {['1', '2', '3'].map(renderNumpadKey)} +
+
+ {['4', '5', '6'].map(renderNumpadKey)} +
+
+ {['7', '8', '9'].map(renderNumpadKey)} +
+
+ {this.renderKey('keyboard-numpad-key', '0', '0̸')} +
+
+ {this.props.showFindButton && ( + + )} +
+
+ ); + } + + /** + * Renders a character key. + * @param cssClass CSS class(es) to apply to the key's root element. + * @param char The character for which to render the key. + * @param label The key's label text. Defaults to the same value as `char`. + * @returns A key for the specified character, as a VNode. + */ + protected renderKey(cssClass: string, char: string, label = char): VNode { + return ( + + ); + } + + /** @inheritdoc */ + public destroy(): void { + this.thisNode && FSComponent.shallowDestroy(this.thisNode); + + if (this.cssClassSub) { + if (Array.isArray(this.cssClassSub)) { + for (const sub of this.cssClassSub) { + sub.destroy(); + } + } else { + this.cssClassSub.destroy(); + } + } + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/index.ts b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/index.ts new file mode 100644 index 000000000..34d310cc6 --- /dev/null +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Keyboard/index.ts @@ -0,0 +1 @@ +export * from './Keyboard'; \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/LatLonInput/LatLonInput.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/LatLonInput/LatLonInput.tsx index 1394b8a97..26397fde9 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/LatLonInput/LatLonInput.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/LatLonInput/LatLonInput.tsx @@ -3,19 +3,26 @@ import { SetSubject, Subject, Subscribable, SubscribableSet, Subscription, VNode } from '@microsoft/msfs-sdk'; + import { LatLonDisplay, LatLonDisplayFormat } from '@microsoft/msfs-garminsdk'; + import { CursorInput } from '../CursorInput/CursorInput'; import { SignInputSlot } from '../NumberInput'; import { DigitInputSlot } from '../NumberInput/DigitInputSlot'; import './LatLonInput.css'; +/** + * Display formats for {@link LatLonInput} supported by the G3000. + */ +export type G3000LatLonDisplayFormat = LatLonDisplayFormat.HDDD_MMmm | LatLonDisplayFormat.HDDD_MM_SSs; + /** * Component props for LatLonInput. */ export interface LatLonInputProps extends ComponentProps { /** The format supported by the input. */ - format: LatLonDisplayFormat; + format: G3000LatLonDisplayFormat; /** * A mutable subscribable to bind to the input's latitude/longitude value. The binding is one-way: changes in the @@ -37,13 +44,19 @@ export class LatLonInput extends DisplayComponent { degreeFactor: 6000, latDigitFactors: [60000, 6000, 1000, 100, 10, 1], lonDigitFactors: [600000, 60000, 6000, 1000, 100, 10, 1], - lonStartIndex: 7 + lonStartIndex: 7, + }, + [LatLonDisplayFormat.HDDD_MMmmm]: { + degreeFactor: 60000, + latDigitFactors: [600000, 60000, 6000, 1000, 100, 10, 1], + lonDigitFactors: [6000000, 600000, 60000, 6000, 1000, 100, 10, 1], + lonStartIndex: 8 }, [LatLonDisplayFormat.HDDD_MM_SSs]: { degreeFactor: 36000, latDigitFactors: [360000, 36000, 6000, 600, 100, 10, 1], lonDigitFactors: [3600000, 360000, 36000, 6000, 600, 100, 10, 1], - lonStartIndex: 8 + lonStartIndex: 8, } }; diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchButton/GtcListSelectTouchButton.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchButton/GtcListSelectTouchButton.tsx index 6b9f8c10b..61f523928 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchButton/GtcListSelectTouchButton.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchButton/GtcListSelectTouchButton.tsx @@ -1,7 +1,7 @@ import { - DisplayComponent, FSComponent, MutableSubscribable, - MutableSubscribableInputType, Subject, SubscribableType, VNode, + DisplayComponent, FSComponent, MutableSubscribable, MutableSubscribableInputType, Subject, VNode, } from '@microsoft/msfs-sdk'; + import { GtcService, GtcViewOcclusionType } from '../../GtcService/GtcService'; import { GtcListDialog, GtcListDialogParams } from '../../Dialog/GtcListDialog'; import { ValueTouchButton } from './ValueTouchButton'; @@ -35,7 +35,7 @@ export interface GtcListSelectTouchButtonProps> | ((state: S) => GtcListDialogParams>); + listParams: GtcListDialogParams> | ((state: S) => GtcListDialogParams>); /** * A callback function which will be called every time a value is selected from the list. If not defined, selecting diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchButton/GtcWaypointSelectButton.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchButton/GtcWaypointSelectButton.tsx index 22bd1b382..a702e3741 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchButton/GtcWaypointSelectButton.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchButton/GtcWaypointSelectButton.tsx @@ -1,12 +1,14 @@ import { - DisplayComponent, Facility, FacilitySearchType, FacilityWaypoint, FSComponent, ICAO, IntersectionFacility, NdbFacility, Subscribable, - SubscribableSet, SubscribableUtils, UserFacility, VNode, VorFacility, + DisplayComponent, Facility, FacilitySearchType, FacilityWaypoint, FSComponent, IntersectionFacility, NdbFacility, SearchTypeMap, Subscribable, + SubscribableSet, SubscribableUtils, UserFacility, VNode, VorFacility } from '@microsoft/msfs-sdk'; + import { AirportWaypoint, GarminFacilityWaypointCache } from '@microsoft/msfs-garminsdk'; + import { GtcService } from '../../GtcService/GtcService'; import { GtcViewKeys } from '../../GtcService/GtcViewKeys'; import { GtcWaypointButton, GtcWaypointButtonProps } from './GtcWaypointButton'; -import { GtcKeyboardDialog } from '../../Dialog/GtcKeyboardDialog'; +import { GtcWaypointDialog } from '../../Dialog/GtcWaypointDialog'; /** * Waypoint search types supported by {@link GtcWaypointSelectButton}. @@ -131,14 +133,11 @@ export class GtcWaypointSelectButton => { const initialWaypoint = this.props.waypoint.get(); - const initialInputText = initialWaypoint === null ? undefined : ICAO.getIdent(initialWaypoint.facility.get().icao); - const result = await this.props.gtcService.openPopup>(GtcViewKeys.KeyboardDialog, 'normal', 'hide') + const result = await this.props.gtcService.openPopup(GtcViewKeys.WaypointDialog, 'normal', 'hide') .ref.request({ - facilitySearchType: this.props.type, - label: GtcWaypointSelectButton.DIALOG_LABEL_TEXT[this.props.type], - allowSpaces: false, - maxLength: 6, - initialInputText + searchType: this.props.type, + emptyLabelText: GtcWaypointSelectButton.DIALOG_LABEL_TEXT[this.props.type], + initialValue: initialWaypoint?.facility.get() as SearchTypeMap[T] }); if (result.wasCancelled) { diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchSlider/TouchSlider.css b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchSlider/TouchSlider.css index 44a1b8074..f08bc7218 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchSlider/TouchSlider.css +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/TouchSlider/TouchSlider.css @@ -1,7 +1,7 @@ html { --touch-slider-background: linear-gradient(#506b85 3px, #2a3947 8px, #141b23 12px, #030405 20px, black 30px); /* The last time I tried ridge borders, it caused some graphical artifacts... */ - --touch-slider-border: 3px ridge #d3d3d3; + --touch-slider-border: 2px solid #ffffff; --touch-slider-border-radius: 4px; --touch-slider-text-color: var(--g3000-color-white); @@ -15,12 +15,16 @@ html { --touch-slider-thumb-justify: 0.5; } +.gtc-vertical { + --touch-slider-border-radius: 3px; +} + .touch-slider { position: relative; - background: var(--touch-button-background); - border: var(--touch-button-border); - border-radius: var(--touch-button-border-radius); - color: var(--touch-button-text-color); + background: var(--touch-slider-background); + border: var(--touch-slider-border); + border-radius: var(--touch-slider-border-radius); + color: var(--touch-slider-text-color); overflow: hidden; display: flex; } diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Waypoint/GtcWaypointDisplay.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Waypoint/GtcWaypointDisplay.tsx index b88c298b9..65a42c06d 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Waypoint/GtcWaypointDisplay.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Components/Waypoint/GtcWaypointDisplay.tsx @@ -3,6 +3,7 @@ import { ICAO, MappedSubject, SetSubject, StringUtils, Subject, Subscribable, SubscribableSet, SubscribableUtils, Subscription, VNode, ComponentProps, } from '@microsoft/msfs-sdk'; + import { GtcWaypointIcon } from '../GtcWaypointIcon/GtcWaypointIcon'; import './GtcWaypointDisplay.css'; @@ -68,21 +69,26 @@ export class GtcWaypointDisplay extends DisplayComponent { - this.facilityPipe?.destroy(); - - if (waypoint === null) { - this.facility.set(null); - } else { - this.facilityPipe = waypoint.facility.pipe(this.facility); - } - }, true); + this.subscriptions.push( + this.waypoint.sub(waypoint => { + this.facilityPipe?.destroy(); + + if (waypoint === null) { + this.facility.set(null); + } else { + this.facilityPipe = waypoint.facility.pipe(this.facility); + } + }, true) + ); } /** @inheritdoc */ @@ -90,8 +96,15 @@ export class GtcWaypointDisplay extends DisplayComponent; if (typeof this.props.class === 'object') { - cssClass = SetSubject.create(['gtc-wpt-display']); - this.cssClassSub = FSComponent.bindCssClassSet(cssClass, this.props.class, GtcWaypointDisplay.RESERVED_CSS_CLASSES); + cssClass = SetSubject.create(); + cssClass.add('gtc-wpt-display'); + + const sub = FSComponent.bindCssClassSet(cssClass, this.props.class, GtcWaypointDisplay.RESERVED_CSS_CLASSES); + if (Array.isArray(sub)) { + this.subscriptions.push(...sub); + } else { + this.subscriptions.push(sub); + } } else { cssClass = 'gtc-wpt-display'; if (this.props.class !== undefined && this.props.class.length > 0) { @@ -115,8 +128,10 @@ export class GtcWaypointDisplay extends DisplayComponent { cssClass.toggle('hidden', !val); }, true); - const maxValue = Math.pow(10, digitCount) - 1; + const maxValue = Math.pow(10, digitCount) * 10 - 1; const valueText = value.map(currentValue => MathUtils.clamp(currentValue / 10, 0, maxValue / 10).toFixed(1)); const leadingZeroes = valueText.map(text => ('').padStart(digitCount - text.length + 2, '0')); diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcFrequencyDialog.css b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcFrequencyDialog.css index 221e78623..f6a5df7c1 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcFrequencyDialog.css +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcFrequencyDialog.css @@ -132,7 +132,7 @@ font-size: var(--frequency-dialog-active-freq-font-size); } -.frequency-dialog-input { +.cursor-input.frequency-dialog-input { position: absolute; left: calc(50% - var(--frequency-dialog-input-width) / 2); top: var(--frequency-dialog-input-top); @@ -143,7 +143,7 @@ --cursor-input-slots-justify: center; } -.frequency-dialog-input.cursor-input-edit-inactive { +.cursor-input.frequency-dialog-input.cursor-input-edit-inactive { color: var(--g3000-color-black); } diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcKeyboardDialog.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcKeyboardDialog.tsx index 29d3f8f3b..4a1dcc25b 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcKeyboardDialog.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcKeyboardDialog.tsx @@ -2,10 +2,9 @@ import { DebounceTimer, Facility, FacilityType, FSComponent, ICAO, IntersectionFacilityUtils, StringUtils, Subject, VNode } from '@microsoft/msfs-sdk'; import { Fms, GarminFacilityWaypointCache, Regions, TouchButton } from '@microsoft/msfs-garminsdk'; -import { ControllableDisplayPaneIndex, G3000WaypointSearchType } from '@microsoft/msfs-wtg3000-common'; +import { G3000WaypointSearchType } from '@microsoft/msfs-wtg3000-common'; import { GtcWaypointIcon } from '../Components/GtcWaypointIcon/GtcWaypointIcon'; -import { GtcControlMode, GtcService, GtcViewLifecyclePolicy } from '../GtcService/GtcService'; import { GtcHardwareControlEvent, GtcInteractionEvent } from '../GtcService/GtcInteractionEvent'; import { GtcView, GtcViewProps } from '../GtcService/GtcView'; import { GtcViewKeys } from '../GtcService/GtcViewKeys'; @@ -61,14 +60,6 @@ export interface GtcKeyboardDialogProps extends GtcViewProps { posHeadingDataProvider: GtcPositionHeadingDataProvider; } -/** - * GTC view keys for popups owned by the keyboard dialog. - */ -enum GtcKeyboardDialogPopupKeys { - FindWaypoint = 'FindWaypoint', - DuplicateWaypoint = 'DuplicateWaypoint' -} - /** Allows user to input text using an alphanumeric keyboard. */ export class GtcKeyboardDialog extends GtcView implements GtcDialogView { @@ -127,14 +118,6 @@ export class GtcKeyboardDialog extends GtcView extends GtcView { const result = await this.props.gtcService - .openPopup(GtcKeyboardDialogPopupKeys.DuplicateWaypoint, 'normal', 'hide') + .openPopup(GtcViewKeys.DuplicateWaypointDialog, 'normal', 'hide') .ref.request({ ident: matches[0].ident, duplicates: matches.map(match => match.facility) }); if (!result.wasCancelled) { @@ -761,24 +744,6 @@ export class GtcKeyboardDialog extends GtcView - ); - } - /** @inheritdoc */ public destroy(): void { this.cleanupRequest(); diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcLatLonDialog.css b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcLatLonDialog.css index 2b5b82c82..5992a0e9e 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcLatLonDialog.css +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcLatLonDialog.css @@ -77,7 +77,7 @@ --latlon-dialog-prefix-button-font-size: 42px; } -.latlon-dialog-input { +.cursor-input.latlon-dialog-input { position: absolute; left: calc(var(--latlon-dialog-input-center-x) - var(--latlon-dialog-input-width) / 2); top: var(--latlon-dialog-input-top); @@ -88,7 +88,7 @@ --cursor-input-slots-justify: center; } -.latlon-dialog-input.cursor-input-edit-inactive { +.cursor-input.latlon-dialog-input.cursor-input-edit-inactive { color: var(--g3000-color-black); } diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcLatLonDialog.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcLatLonDialog.tsx index 418a374b3..ce8b8235e 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcLatLonDialog.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcLatLonDialog.tsx @@ -5,7 +5,7 @@ import { GtcImgTouchButton } from '../Components/TouchButton/GtcImgTouchButton'; import { GtcInteractionEvent } from '../GtcService/GtcInteractionEvent'; import { GtcView } from '../GtcService/GtcView'; import { GtcDialogResult, GtcDialogView } from './GtcDialogView'; -import { LatLonInput } from '../Components/LatLonInput/LatLonInput'; +import { G3000LatLonDisplayFormat, LatLonInput } from '../Components/LatLonInput/LatLonInput'; import { GtcDialogs } from './GtcDialogs'; import { GtcTouchButton } from '../Components'; @@ -16,7 +16,7 @@ import './GtcLatLonDialog.css'; */ export type GtcLatLonDialogInput = { /** The input format type to use. */ - format: LatLonDisplayFormat; + format: G3000LatLonDisplayFormat; /** The latitude/longitude coordinates initially loaded into the dialog at the start of the request. */ initialValue: LatLonInterface; @@ -61,7 +61,7 @@ export class GtcLatLonDialog extends GtcView implements GtcDialogView = { + private readonly contexts: Record = { [LatLonDisplayFormat.HDDD_MMmm]: { inputRef: FSComponent.createRef(), cssClass: SetSubject.create(['latlon-dialog-input', 'latlon-dialog-input-dddmm', 'hidden']), @@ -102,7 +102,7 @@ export class GtcLatLonDialog extends GtcView implements GtcDialogView { this.onEditingActiveChanged(isActive); }); diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcMessageDialog.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcMessageDialog.tsx index 3af158714..280b6e399 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcMessageDialog.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcMessageDialog.tsx @@ -97,7 +97,7 @@ export class GtcMessageDialog extends GtcView implements GtcDialogView cssClass !== 'gtc-message-dialog'); + this.cssClassesToAdd = FSComponent.parseCssClassesFromString(cssClassesToAdd).filter(cssClass => cssClass !== 'message-dialog'); for (const cssClass of this.cssClassesToAdd) { this.rootCssClass.add(cssClass); diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcUserWaypointDialog.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcUserWaypointDialog.tsx index e69e6b397..43b708a24 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcUserWaypointDialog.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcUserWaypointDialog.tsx @@ -3,9 +3,11 @@ import { FSComponent, GeoPointSubject, ICAO, LatLonInterface, MagVar, NearestSubscription, NumberFormatter, NumberUnitSubject, SetSubject, Subject, UnitFamily, UnitType, UserFacility, UserFacilityUtils, VNode, VorFacility, } from '@microsoft/msfs-sdk'; + import { BearingDisplay, LatLonDisplay, LatLonDisplayFormat, NumberUnitDisplay, UnitsDistanceSettingMode, UnitsUserSettings, } from '@microsoft/msfs-garminsdk'; + import { G3000NearestContext } from '@microsoft/msfs-wtg3000-common'; import { GtcListSelectTouchButton } from '../Components/TouchButton/GtcListSelectTouchButton'; import { GtcToggleTouchButton } from '../Components/TouchButton/GtcToggleTouchButton'; @@ -16,13 +18,14 @@ import { GtcView, GtcViewProps } from '../GtcService/GtcView'; import { GtcViewKeys } from '../GtcService/GtcViewKeys'; import { GtcPositionHeadingDataProvider } from '../Navigation/GtcPositionHeadingDataProvider'; import { GtcUserWaypointEditController, UserWaypointFlightPlanStatus } from '../Navigation/GtcUserWaypointEditController'; -import { GtcKeyboardDialog } from './GtcKeyboardDialog'; import { GtcCourseDialog } from './GtcCourseDialog'; import { GtcDialogs } from './GtcDialogs'; import { GtcDialogResult, GtcDialogView } from './GtcDialogView'; import { GtcDistanceDialog } from './GtcDistanceDialog'; +import { GtcKeyboardDialog } from './GtcKeyboardDialog'; import { GtcLatLonDialog } from './GtcLatLonDialog'; import { GtcUserWaypointDialogStore, GtcUserWaypointType } from './GtcUserWaypointDialogStore'; +import { GtcWaypointDialog } from './GtcWaypointDialog'; import './GtcUserWaypointDialog.css'; @@ -390,13 +393,11 @@ export class GtcUserWaypointDialog extends GtcView i private async selectReference(subject: Subject): Promise { const initialValue = subject.get(); - const result = await this.props.gtcService.openPopup>(GtcViewKeys.KeyboardDialog, 'normal', 'hide') + const result = await this.props.gtcService.openPopup(GtcViewKeys.WaypointDialog, 'normal', 'hide') .ref.request({ - facilitySearchType: FacilitySearchType.AllExceptVisual, - initialInputText: initialValue === null ? undefined : ICAO.getIdent(initialValue.icao), - label: 'Waypoint Identifier Lookup', - maxLength: 6, - allowSpaces: false + searchType: FacilitySearchType.AllExceptVisual, + emptyLabelText: 'Waypoint Identifier Lookup', + initialValue: initialValue ?? undefined }); if (!result.wasCancelled) { diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcWaypointDialog.css b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcWaypointDialog.css new file mode 100644 index 000000000..a8194f45a --- /dev/null +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcWaypointDialog.css @@ -0,0 +1,126 @@ +.wpt-dialog { + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 100%; + color: var(--g3000-color-white); +} + +.gtc-horizontal .wpt-dialog { + --cursor-input-slots-padding: 0px 12px; + + --wpt-dialog-top-section-width: 893px; + --wpt-dialog-top-section-height: 78px; + + --wpt-dialog-input-width: 297px; + --wpt-dialog-input-height: 62px; + --wpt-dialog-input-margin: 0px 0px 0px 16px; + --wpt-dialog-input-font-size: 48px; + + --wpt-dialog-input-label-margin: 0px 10px 0px 22px; + --wpt-dialog-input-label-font-size: 33px; + + --wpt-dialog-input-icon-margin: 0px 57px 0px 0px; + --wpt-dialog-input-icon-scale: 1.8; + + --wpt-dialog-keyboard-left: 61px; + --wpt-dialog-keyboard-top: 81px; +} + +.gtc-vertical .wpt-dialog { + --cursor-input-slots-padding: 0px 7px; + + --wpt-dialog-top-section-width: 100%; + --wpt-dialog-top-section-height: 42px; + + --wpt-dialog-input-width: 160px; + --wpt-dialog-input-height: 34px; + --wpt-dialog-input-margin: 0px 0px 0px 8px; + --wpt-dialog-input-font-size: 27px; + + --wpt-dialog-input-label-margin: 0px 3px 0px 11px; + --wpt-dialog-input-label-font-size: 18px; + + --wpt-dialog-input-icon-margin: 0px 13px 0px 0px; + --wpt-dialog-input-icon-scale: 1; + + --wpt-dialog-keyboard-left: 7px; + --wpt-dialog-keyboard-top: 44px; +} + +.wpt-dialog-top-section { + position: absolute; + left: 50%; + top: 0px; + width: var(--wpt-dialog-top-section-width); + height: var(--wpt-dialog-top-section-height); + transform: translateX(-50%); + background-color: var(--g3000-color-black); + display: flex; + flex-flow: row nowrap; + align-items: center; +} + +.wpt-dialog-input { + flex-shrink: 0; + width: var(--wpt-dialog-input-width); + height: var(--wpt-dialog-input-height); + margin: var(--wpt-dialog-input-margin); + color: var(--g3000-color-cyan); + font-size: var(--wpt-dialog-input-font-size); +} + +.wpt-dialog-input.cursor-input-edit-inactive { + color: #003333; +} + +.wpt-dialog-input .cursor-input-inactive { + position: absolute; + left: 0px; + top: 50%; + width: 100%; + height: auto; + padding: var(--cursor-input-slots-padding); + transform: translateY(-50%); + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; +} + +.wpt-dialog-input-inactive-value-text { + background: var(--g3000-color-cyan); +} + +.wpt-dialog-input-inactive-value-text-empty { + color: var(--g3000-color-black); +} + +.wpt-dialog-input-slot-autocomplete .cursor-input-slot-character.cursor-input-slot-character-empty { + color: #003333; + animation: none; +} + +.wpt-dialog-input-label { + flex-grow: 1; + height: 1.2em; + margin: var(--wpt-dialog-input-label-margin); + font-size: var(--wpt-dialog-input-label-font-size); + line-height: 1.2em; + overflow: hidden; + /* This makes it so the text doesn't get clipped in the middle of a character. */ + word-break: break-all; +} + +.wpt-dialog-input-icon { + flex-shrink: 0; + margin: var(--wpt-dialog-input-icon-margin); + transform: scale(var(--wpt-dialog-input-icon-scale)); + transform-origin: 0% 50%; +} + +.wpt-dialog .wpt-dialog-keyboard { + position: absolute; + left: var(--wpt-dialog-keyboard-left); + top: var(--wpt-dialog-keyboard-top); +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcWaypointDialog.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcWaypointDialog.tsx new file mode 100644 index 000000000..6896953e7 --- /dev/null +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/GtcWaypointDialog.tsx @@ -0,0 +1,549 @@ +import { + ArrayUtils, DebounceTimer, Facility, FacilityType, FSComponent, ICAO, IntersectionFacilityUtils, MappedSubject, + NodeReference, SearchTypeMap, Subject, VNode +} from '@microsoft/msfs-sdk'; + +import { Fms, GarminFacilityWaypointCache, Regions } from '@microsoft/msfs-garminsdk'; + +import { G3000WaypointSearchType } from '@microsoft/msfs-wtg3000-common'; + +import { CharInput } from '../Components/CharInput/CharInput'; +import { CharInputSlot } from '../Components/CharInput/CharInputSlot'; +import { GtcWaypointIcon } from '../Components/GtcWaypointIcon/GtcWaypointIcon'; +import { Keyboard } from '../Components/Keyboard/Keyboard'; +import { GtcHardwareControlEvent, GtcInteractionEvent } from '../GtcService/GtcInteractionEvent'; +import { GtcView, GtcViewProps } from '../GtcService/GtcView'; +import { GtcViewKeys } from '../GtcService/GtcViewKeys'; +import { GtcDialogResult, GtcDialogView } from './GtcDialogView'; +import { GtcDuplicateWaypointDialog } from './GtcDuplicateWaypointDialog'; +import { GtcFindWaypointDialog } from './GtcFindWaypointDialog'; + +import './GtcWaypointDialog.css'; + +/** + * A request input for {@link GtcWaypointDialog}. + */ +export interface GtcWaypointDialogInput { + /** The type of waypoint to search for. */ + searchType: T; + + /** The initial label text to display when the dialog's identifier input is empty. */ + emptyLabelText: string; + + /** The waypoint value initially loaded into the dialog at the start of the request. */ + initialValue?: SearchTypeMap[T] | null; +} + +/** + * Component props for GtcWaypointDialog. + */ +export interface GtcWaypointDialogProps extends GtcViewProps { + /** The Fms instance to use. */ + fms: Fms; +} + +/** + * An entry for a single character input slot. + */ +type CharInputSlotEntry = { + /** A reference to the input slot. */ + ref: NodeReference; + + /** The input slot's default character value. */ + defaultCharValue: Subject; +}; + +/** + * A search result. + */ +interface SearchResult { + /** The ICAO. */ + readonly icao: string; + + /** The ident. */ + readonly ident: string; +} + +/** + * A search result, also with a facility. + */ +interface SearchResultWithFacility extends SearchResult { + /** The facility. */ + readonly facility: Facility; +} + +/** + * Results of a facility search. + */ +interface SearchResults { + /** Matches where the ident exactly matches the current user input. */ + readonly exactMatches?: readonly SearchResultWithFacility[]; + + /** The first suggested partial match given the current user input. */ + readonly suggestedMatch?: SearchResultWithFacility; +} + +/** + * A dialog which allows the user to select a waypoint. + */ +export class GtcWaypointDialog extends GtcView + implements GtcDialogView, Facility> { + + private static readonly CHAR_ARRAY = [ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '' + ]; + + private thisNode?: VNode; + + private readonly inputRef = FSComponent.createRef(); + private readonly keyboardRef = FSComponent.createRef(); + + private readonly inputSlotEntries: CharInputSlotEntry[] = ArrayUtils.create(6, () => { + return { + ref: FSComponent.createRef(), + defaultCharValue: Subject.create('') + }; + }); + + private readonly inputText = Subject.create(''); + private readonly inputTextSub = this.inputText.sub(this.onInputTextChanged.bind(this, true), false, true); + + private readonly autocompleteText = Subject.create(''); + private readonly autocompleteTextSub = MappedSubject.create( + this.inputText, + this.autocompleteText + ).sub(this.updateAutocomplete.bind(this), false, true); + + private readonly inputLabelText = Subject.create(''); + + private readonly selectedFacility = Subject.create(null); + private readonly waypoint = this.selectedFacility + .map(x => x ? GarminFacilityWaypointCache.getCache(this.bus).get(x) : null); + + private facilityMatches?: readonly SearchResultWithFacility[]; + + private readonly searchDebounce = new DebounceTimer(); + + private searchOpId = 0; + + private facilitySearchType?: G3000WaypointSearchType; + + private resolveFunction?: (value: any) => void; + private resultObject: GtcDialogResult = { + wasCancelled: true, + }; + + /** @inheritDoc */ + public onAfterRender(thisNode: VNode): void { + this.thisNode = thisNode; + + this._sidebarState.slot5.set('enterEnabled'); + this._sidebarState.dualConcentricKnobLabel.set('dataEntryPushEnter'); + } + + /** @inheritDoc */ + public request(input: GtcWaypointDialogInput): Promise> { + return new Promise>(resolve => { + this.cleanupRequest(); + + this.resolveFunction = resolve; + this.resultObject = { + wasCancelled: true, + }; + + this.facilitySearchType = input.searchType; + + if (input.initialValue) { + this.inputRef.instance.setValue(ICAO.getIdent(input.initialValue.icao)); + this.selectedFacility.set(input.initialValue); + this.inputLabelText.set(this.getFacilityLabel(input.initialValue)); + } else { + this.inputRef.instance.setValue(''); + this.selectedFacility.set(null); + this.inputLabelText.set(input.emptyLabelText); + } + + this.autocompleteText.set(''); + + this.inputRef.instance.deactivateEditing(); + this.inputRef.instance.refresh(); + + this._sidebarState.slot1.set(null); + this.keyboardRef.instance.setShowNumpad(false); + + this.inputTextSub.resume(); + this.autocompleteTextSub.resume(true); + }); + } + + /** @inheritdoc */ + public onClose(): void { + this.cleanupRequest(); + } + + /** @inheritdoc */ + public onGtcInteractionEvent(event: GtcInteractionEvent): boolean { + switch (event) { + case GtcHardwareControlEvent.InnerKnobInc: + this.inputRef.instance.changeSlotValue(1, true); + return true; + case GtcHardwareControlEvent.InnerKnobDec: + this.inputRef.instance.changeSlotValue(-1, true); + return true; + case GtcHardwareControlEvent.OuterKnobInc: + this.inputRef.instance.moveCursor(1, true); + return true; + case GtcHardwareControlEvent.OuterKnobDec: + this.inputRef.instance.moveCursor(-1, true); + return true; + case GtcHardwareControlEvent.InnerKnobPush: + case GtcHardwareControlEvent.InnerKnobPushLong: + case GtcInteractionEvent.ButtonBarEnterPressed: + this.resolve(); + return true; + default: + return false; + } + } + + private readonly updateSearchHandler = this.updateSearch.bind(this); + + /** + * A callback called when the search input box is updated. + * @param debounce Whether to debounce the call to update autocomplete. + */ + private onInputTextChanged(debounce = false): void { + this._sidebarState.slot1.set('cancel'); + + if (this.facilitySearchType === undefined) { + return; + } + + this.searchDebounce.clear(); + + if (this.inputText.get() === '') { + this.inputLabelText.set('No matches found'); + this.selectedFacility.set(null); + this.autocompleteText.set(''); + } else { + if (debounce) { + this.searchDebounce.schedule(this.updateSearchHandler, 250); + } else { + this.updateSearch(); + } + } + } + + /** + * Checks for matches with current input, and updates the label and suggested text. + */ + private async updateSearch(): Promise { + const opId = ++this.searchOpId; + + const { exactMatches, suggestedMatch } = await this.searchFacilities(this.inputText.get(), opId); + + if (opId !== this.searchOpId) { + return; + } + + if (exactMatches) { + this.facilityMatches = exactMatches; + + if (exactMatches.length === 1) { + this.inputLabelText.set(this.getFacilityLabel(exactMatches[0].facility)); + this.selectedFacility.set(exactMatches[0].facility); + } else if (exactMatches.length > 1) { + this.inputLabelText.set('Duplicates found'); + this.selectedFacility.set(null); + } + + this.autocompleteText.set(''); + + } else if (suggestedMatch) { + this.facilityMatches = undefined; + + this.inputLabelText.set(this.getFacilityLabel(suggestedMatch.facility)); + this.selectedFacility.set(suggestedMatch.facility); + this.autocompleteText.set(suggestedMatch.ident); + } else { + this.inputLabelText.set('No matches found'); + this.selectedFacility.set(null); + this.autocompleteText.set(''); + } + } + + /** + * Searches facilities with a given ident and returns matches. + * @param searchString The ident to search. + * @param opId The search operation ID. + * @returns A Promise which will be fulfilled with the results of the facility search. + */ + private async searchFacilities(searchString: string, opId: number): Promise { + if (this.facilitySearchType === undefined) { + throw new Error('facility search type is required keyboard param when searching facilities.'); + } + + const allMatches = await this.props.fms.facLoader.searchByIdent(this.facilitySearchType, searchString, 20); + + if (opId !== this.searchOpId) { + return {}; + } + + const exactMatches = allMatches.filter(match => ICAO.getIdent(match) === searchString); + if (exactMatches.length > 0) { + return { + exactMatches: await Promise.all( + // Filter out any terminal intersections that are duplicates of non-terminal intersection matches. + IntersectionFacilityUtils.filterDuplicates(exactMatches) + .map>(async match => { + return { + icao: match, + ident: ICAO.getIdent(match), + facility: await this.props.fms.facLoader.getFacility(ICAO.getFacilityType(match), match) as Facility, + }; + }) + ) + }; + } else if (allMatches.length > 0) { + let firstMatch = allMatches[0]; + + // Check if the first match is a terminal duplicate of a non-terminal intersection match. If it is, replace it + // with the non-terminal version. + if (ICAO.isFacility(firstMatch, FacilityType.Intersection) && IntersectionFacilityUtils.isTerminal(firstMatch)) { + const nonTerminalIcao = IntersectionFacilityUtils.getNonTerminalICAO(firstMatch); + if (allMatches.includes(nonTerminalIcao)) { + firstMatch = nonTerminalIcao; + } + } + + return { + suggestedMatch: { + icao: firstMatch, + ident: ICAO.getIdent(firstMatch), + facility: await this.props.fms.facLoader.getFacility(ICAO.getFacilityType(firstMatch), firstMatch) as Facility, + } + }; + } else { + return {}; + } + } + + /** + * Get the label text to display for a facility. + * @param facility The facility for which to get label text. + * @returns The label text to display for the specified facility. + */ + private getFacilityLabel(facility: Facility): string { + const facilityType = ICAO.getFacilityType(facility.icao); + + if (facilityType === FacilityType.Airport) { + return Utils.Translate(facility.name); + } else { + if (facility.city.length > 0) { + const separatedCity = facility.city.split(', '); + const city = separatedCity.length > 1 ? Utils.Translate(separatedCity[0]) + ', ' + Utils.Translate(separatedCity[1]) : Utils.Translate(facility.city); + if (city) { + return city; + } + } + + const regionCode = ICAO.getRegionCode(facility.icao); + if (regionCode) { + return Regions.getName(regionCode); + } + + const name = Utils.Translate(facility.name); + if (name) { + return name; + } + + return `${facility.lat.toFixed(4)}, ${facility.lon.toFixed(4)}`; + } + } + + /** + * Updates the default character values of this dialog's character input to match the current autocomplete state. + * @param root0 The current autocomplete state. + * @param root0."0" The current input text. + * @param root0."1" The current autocomplete text. + */ + private updateAutocomplete([inputText, autocompleteText]: readonly [string, string]): void { + let endIndex = autocompleteText.length; + + if (autocompleteText === '' || autocompleteText.length < inputText.length || !autocompleteText.startsWith(inputText)) { + endIndex = 0; + } + + for (let i = 0; i < this.inputSlotEntries.length; i++) { + if (i < endIndex) { + this.inputSlotEntries[i].defaultCharValue.set(autocompleteText[i]); + } else { + this.inputSlotEntries[i].defaultCharValue.set(''); + } + } + } + + /** + * Attempts to resolve the current request. + * + * If this dialog searches for facilities, then the currently selected facility will be returned if one exists. If + * there is no selected facility, duplicate matches will attempted to be resolved if they exist. If neither a + * selected facility or duplicate matches exist, the request will be cancelled. + * + * If this dialog does not search for facilities, the current input text is returned. + */ + private resolve(): void { + if (this.facilitySearchType === undefined) { + return; + } + + const facility = this.selectedFacility.get(); + if (facility) { + this.resultObject = { + wasCancelled: false, + payload: facility + }; + + this.props.gtcService.goBack(); + } else if (this.facilityMatches !== undefined) { + this.resolveDuplicates(this.facilityMatches); + } else { + this.props.gtcService.goBack(); + } + } + + /** + * Attempts to resolve duplicate matched facilities. Opens the duplicate waypoint dialog to allow the user to + * select one of the duplicates. If the user selects a duplicate, the current request will be resolved with the + * selected facility and this dialog will be closed. If the user does not select a duplicate, the current request + * will remain unresolved and this dialog will remain open. + * @param matches The search results of the duplicate matched facilities. + */ + private async resolveDuplicates(matches: readonly SearchResultWithFacility[]): Promise { + const result = await this.props.gtcService + .openPopup(GtcViewKeys.DuplicateWaypointDialog, 'normal', 'hide') + .ref.request({ ident: matches[0].ident, duplicates: matches.map(match => match.facility) }); + + if (!result.wasCancelled) { + this.resultObject = { + wasCancelled: false, + payload: result.payload + }; + + this.props.gtcService.goBack(); + } + } + + /** + * Clears this dialog's pending request and fulfills the pending request Promise if one exists. + */ + private cleanupRequest(): void { + this.inputTextSub.pause(); + this.autocompleteTextSub.pause(); + this.facilitySearchType = undefined; + this.facilityMatches = undefined; + + const resolve = this.resolveFunction; + this.resolveFunction = undefined; + resolve && resolve(this.resultObject); + } + + /** + * Responds to when one of this dialog's character keys is pressed. + * @param char The character of the key that was pressed. + */ + private onKeyPressed(char: string): void { + this.inputRef.instance.setSlotCharacterValue(char); + } + + /** + * Responds to when this dialog's backspace button is pressed. + */ + private onBackspacePressed(): void { + this.inputRef.instance.backspace(); + } + + /** + * Responds to when this dialog's Find button is pressed. + */ + private async onFindPressed(): Promise { + if (this.facilitySearchType === undefined) { + return; + } + + const result = await this.gtcService.openPopup(GtcViewKeys.FindWaypointDialog) + .ref.request(this.facilitySearchType); + + if (!result.wasCancelled) { + this.selectedFacility.set(result.payload.facility.get()); + this.resolve(); + } + } + + /** @inheritdoc */ + public render(): VNode { + return ( +
+
+ text === '') + }} + > + {this.inputText.map(text => text === '' ? '______' : text)} +
+ } + class='wpt-dialog-input' + > + {this.inputSlotEntries.map(entry => { + return ( + value !== '') + }} + /> + ); + })} + +
+ {this.inputLabelText} +
+
waypoint === null) }}> + +
+
+ +
+ ); + } + + /** @inheritdoc */ + public destroy(): void { + this.cleanupRequest(); + + this.searchDebounce.clear(); + + this.thisNode && FSComponent.shallowDestroy(this.thisNode); + + super.destroy(); + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/index.ts b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/index.ts index 36166bd0f..eff95c65c 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/index.ts +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Dialog/index.ts @@ -30,4 +30,5 @@ export * from './GtcTemperatureDialog'; export * from './GtcUserWaypointDialog'; export * from './GtcVnavAltitudeDialog'; export * from './GtcVnavFlightPathAngleDialog'; +export * from './GtcWaypointDialog'; export * from './GtcWeightDialog'; \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/GtcService/GtcViewKeys.ts b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/GtcService/GtcViewKeys.ts index 8742137b0..8e9c18b3d 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/GtcService/GtcViewKeys.ts +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/GtcService/GtcViewKeys.ts @@ -77,5 +77,7 @@ export enum GtcViewKeys { FmsSpeedDialog = 'FmsSpeedDialog', MinuteDurationDialog = 'MinuteDurationDialog', ToldFactorDialog = 'ToldFactorDialog', - FindWaypointDialog = 'FindWaypointDialog' + WaypointDialog = 'WaypointDialog', + FindWaypointDialog = 'FindWaypointDialog', + DuplicateWaypointDialog = 'DuplicateWaypointDialog' } \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/AvionicsSettingsPage/GtcAvionicsSettingsPageMfdFieldsList.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/AvionicsSettingsPage/GtcAvionicsSettingsPageMfdFieldsList.tsx index a77155037..b88323271 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/AvionicsSettingsPage/GtcAvionicsSettingsPageMfdFieldsList.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/AvionicsSettingsPage/GtcAvionicsSettingsPageMfdFieldsList.tsx @@ -35,7 +35,7 @@ export interface GtcAvionicsSettingsPageMfdFieldsListProps extends ComponentProp * A GTC avionics setting page MFD fields settings list. */ export class GtcAvionicsSettingsPageMfdFieldsList extends DisplayComponent implements GtcAvionicsSettingsPageTabContent { - private static readonly LONG_NAMES: Record = { + private static readonly LONG_NAMES: Partial> = { [NavDataFieldType.BearingToWaypoint]: 'Bearing', [NavDataFieldType.Destination]: 'Destination Airport', [NavDataFieldType.DistanceToWaypoint]: 'Distance', @@ -54,12 +54,10 @@ export class GtcAvionicsSettingsPageMfdFieldsList extends DisplayComponent fieldType !== NavDataFieldType.Waypoint) .map(fieldType => { return { value: fieldType as NavDataFieldType, diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/FlightPlanPage/GtcFlightPlanDialogs.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/FlightPlanPage/GtcFlightPlanDialogs.tsx index 09ef9daf1..deaa77a98 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/FlightPlanPage/GtcFlightPlanDialogs.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/FlightPlanPage/GtcFlightPlanDialogs.tsx @@ -6,32 +6,40 @@ import { import { ApproachNameDisplay, Fms, FmsUtils } from '@microsoft/msfs-garminsdk'; import { FlightPlanListManager, FlightPlanStore } from '@microsoft/msfs-wtg3000-common'; + import { GtcAirwaySelectionDialog } from '../../Dialog/GtcAirwaySelectionDialog'; import { GtcDialogs } from '../../Dialog/GtcDialogs'; import { GtcDialogResult } from '../../Dialog/GtcDialogView'; -import { GtcKeyboardDialog } from '../../Dialog/GtcKeyboardDialog'; import { GtcListDialog, ListDialogItemDefinition } from '../../Dialog/GtcListDialog'; +import { GtcWaypointDialog } from '../../Dialog/GtcWaypointDialog'; import { GtcService } from '../../GtcService/GtcService'; import { GtcViewKeys } from '../../GtcService/GtcViewKeys'; - /** Collection of utility functions to open different flight plan related dialogs. */ export class GtcFlightPlanDialogs { /** - * Opens an Airport Identifier Lookup keyboard dialog. + * Opens an Airport Identifier Lookup waypoint dialog. + * @param gtcService The GtcService. + * @param initialValue The airport value initially loaded into the dialog at the start of the request. + * @returns The selected airport, if one was selected. + */ + public static openAirportDialog(gtcService: GtcService, initialValue?: AirportFacility): Promise>; + /** + * Opens an Airport Identifier Lookup waypoint dialog. * @param gtcService The GtcService. - * @param initialInputText Initial input text, if it should show an airport as already beign typed out. + * @param initialValue This parameter is ignored. * @returns The selected airport, if one was selected. + * @deprecated */ - public static openAirportDialog(gtcService: GtcService, initialInputText?: string): Promise> { + public static openAirportDialog(gtcService: GtcService, initialValue?: string): Promise>; + // eslint-disable-next-line jsdoc/require-jsdoc + public static openAirportDialog(gtcService: GtcService, initialValue?: AirportFacility | string): Promise> { return gtcService - .openPopup>(GtcViewKeys.KeyboardDialog, 'normal', 'hide') + .openPopup(GtcViewKeys.WaypointDialog, 'normal', 'hide') .ref.request({ - facilitySearchType: FacilitySearchType.Airport, - label: 'Airport Identifier Lookup', - allowSpaces: false, - maxLength: 6, - initialInputText, + searchType: FacilitySearchType.Airport, + emptyLabelText: 'Airport Identifier Lookup', + initialValue: typeof initialValue === 'object' ? initialValue : undefined, }); } @@ -267,12 +275,11 @@ export class GtcFlightPlanDialogs { * @returns The result of the keyboard dialog request. */ public static async openWaypointIdentifierLookup(gtcService: GtcService): Promise> { - return gtcService.openPopup>( - GtcViewKeys.KeyboardDialog, 'normal', 'hide').ref.request({ - facilitySearchType: FacilitySearchType.AllExceptVisual, - label: 'Waypoint Identifier Lookup', - allowSpaces: false, - maxLength: 6, + return gtcService + .openPopup(GtcViewKeys.WaypointDialog, 'normal', 'hide') + .ref.request({ + searchType: FacilitySearchType.AllExceptVisual, + emptyLabelText: 'Waypoint Identifier Lookup' }); } diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/NavComHome/GtcAudioRadiosPopup.css b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/NavComHome/GtcAudioRadiosPopup.css index 43116fc6c..b7ee29fc1 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/NavComHome/GtcAudioRadiosPopup.css +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/NavComHome/GtcAudioRadiosPopup.css @@ -1,5 +1,6 @@ .audio-radios-popup { --touch-button-border: 1px solid #d3d3d3; + --touch-slider-border: 1px solid #d3d3d3; } .gtc-horizontal .audio-radios-popup { diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/Procedures/GtcProcedureSelectionPage.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/Procedures/GtcProcedureSelectionPage.tsx index 33cdccc33..1a5a98893 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/Procedures/GtcProcedureSelectionPage.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/Pages/Procedures/GtcProcedureSelectionPage.tsx @@ -176,8 +176,7 @@ export abstract class GtcProcedureSelectionPage

=> { const airport = this.selectedAirport.get(); - const initialText = airport ? ICAO.getIdent(airport.icao) : undefined; - const result = await GtcFlightPlanDialogs.openAirportDialog(this.gtcService, initialText); + const result = await GtcFlightPlanDialogs.openAirportDialog(this.gtcService, airport); if (!result.wasCancelled) { this.selectedAirport.set(result.payload); onAirportSelected(); diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/WTG3000GtcInstrument.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/WTG3000GtcInstrument.tsx index e9a8b6dea..82ee18739 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/WTG3000GtcInstrument.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/WTG3000GtcInstrument.tsx @@ -1,12 +1,16 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { + DefaultObsSuspDataProvider, DefaultVNavDataProvider, Fms, GarminFacilityWaypointCache, GpsNavSource, + NavRadioNavSource, + NavReferenceIndicatorsCollection, + NavReferenceSource, + NavReferenceSourceCollection, + TrafficSystemType, +} from '@microsoft/msfs-garminsdk'; import { ClockEvents, FacilityWaypoint, FSComponent, IntersectionFacility, NdbFacility, PluginSystem, SetSubject, Subject, UserFacility, VNode, VorFacility, Wait, XPDRSimVarPublisher, } from '@microsoft/msfs-sdk'; -import { - DefaultObsSuspDataProvider, DefaultVNavDataProvider, Fms, GarminFacilityWaypointCache, GpsNavSource, - NavReferenceIndicatorsCollection, NavRadioNavSource, NavReferenceSource, TrafficSystemType, NavReferenceSourceCollection, -} from '@microsoft/msfs-garminsdk'; import { AvionicsConfig, AvionicsStatus, AvionicsStatusChangeEvent, DefaultFmsSpeedTargetDataProvider, ExistingUserWaypointsArray, FlightPlanListManager, FlightPlanStore, G3000ActiveSourceNavIndicator, G3000FilePaths, G3000NavIndicator, G3000NavIndicatorName, G3000NavIndicators, G3000NavSourceName, @@ -16,10 +20,11 @@ import { import { LabelBarPluginHandlers } from './Components'; import { GtcConfig } from './Config'; import { - GtcAirwaySelectionDialog, GtcAltitudeDialog, GtcBaroPressureDialog, GtcCourseDialog, GtcDistanceDialog, GtcDurationDialog, + GtcAirwaySelectionDialog, GtcAltitudeDialog, GtcBaroPressureDialog, GtcCourseDialog, GtcDistanceDialog, GtcDuplicateWaypointDialog, GtcDurationDialog, GtcDurationDialogMSS, GtcFindWaypointDialog, GtcFmsSpeedDialog, GtcFrequencyDialog, GtcIntegerDialog, GtcKeyboardDialog, GtcLatLonDialog, GtcListDialog, GtcLoadFrequencyDialog, GtcLocalTimeOffsetDialog, GtcMessageDialog, GtcMinimumsSourceDialog, GtcMinuteDurationDialog, GtcRunwayLengthDialog, - GtcSpeedConstraintDialog, GtcSpeedDialog, GtcTemperatureDialog, GtcUserWaypointDialog, GtcVnavAltitudeDialog, GtcVnavFlightPathAngleDialog, GtcWeightDialog, + GtcSpeedConstraintDialog, GtcSpeedDialog, GtcTemperatureDialog, GtcUserWaypointDialog, GtcVnavAltitudeDialog, GtcVnavFlightPathAngleDialog, + GtcWaypointDialog, GtcWeightDialog } from './Dialog'; import { G3000GtcPlugin, G3000GtcPluginBinder } from './G3000GTCPlugin'; import { G3000GtcViewContext } from './G3000GtcViewContext'; @@ -140,7 +145,9 @@ export class WTG3000GtcInstrument extends WTG3000FsInstrument { this.backplane.addPublisher(InstrumentBackplaneNames.Xpdr, this.xpdrSimVarPublisher); - this.doInit(); + this.doInit().catch(e => { + console.error(e); + }); } /** @@ -1016,6 +1023,22 @@ export class WTG3000GtcInstrument extends WTG3000FsInstrument { ); } ); + + this.gtcService.registerView( + GtcViewLifecyclePolicy.Transient, + GtcViewKeys.WaypointDialog, 'MFD', + (gtcService, controlMode, displayPaneIndex) => { + return ( + + ); + } + ); + this.gtcService.registerView( GtcViewLifecyclePolicy.Persistent, GtcViewKeys.FindWaypointDialog, 'MFD', @@ -1029,6 +1052,21 @@ export class WTG3000GtcInstrument extends WTG3000FsInstrument { ) ); + this.gtcService.registerView( + GtcViewLifecyclePolicy.Transient, + GtcViewKeys.DuplicateWaypointDialog, 'MFD', + (gtcService, controlMode, displayPaneIndex) => { + return ( + + ); + } + ); + this.gtcService.registerView( GtcViewLifecyclePolicy.Transient, GtcViewKeys.FrequencyDialog, 'NAV_COM', diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/package-defs-only.json b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/package-defs-only.json index fbcebbbe7..4c927f774 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/package-defs-only.json +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/package-defs-only.json @@ -1,6 +1,6 @@ { "name": "@microsoft/msfs-wtg3000-gtc", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000 GTC Definitions", "typings": "index.d.ts", "repository": { diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/package.json b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/package.json index f577630a6..e01a67cbe 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/package.json +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/package.json @@ -1,18 +1,15 @@ { "name": "@microsoft/msfs-wtg3000-gtc", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000 GTC Definitions", "typings": "index.d.ts", "scripts": { - "build:defs": "tsc & robocopy . *.css build /s & rollup -c rollup-defs.config.js & cp package-defs-only.json dist/package.json & rm dist/ignoreme.css" + "build:defs": "tsc && cp package-defs-only.json build/package.json" }, "repository": { "type": "git", "url": "https://github.com/microsoft/msfs-avionics-mirror/" }, "author": "Working Title Simulations, LLC", - "license": "Modified MIT", - "dependencies": { - "rollup": "^2.79.1" - } -} + "license": "Modified MIT" +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/rollup-defs.config.js b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/rollup-defs.config.js deleted file mode 100644 index 9cb07a430..000000000 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/rollup-defs.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import dts from 'rollup-plugin-dts' -import css from 'rollup-plugin-import-css'; -import resolve from '@rollup/plugin-node-resolve'; - -const config = [ - { - input: 'build/index.d.ts', - output: [{ file: 'dist/index.d.ts', format: 'es' }], - plugins: [dts(), resolve(), css({ output: 'ignoreme.css' })], - } -] -export default config \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/tsconfig.json b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/tsconfig.json index 410204972..1afd04606 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/tsconfig.json +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/GTC/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "build", - "emitDeclarationOnly": false + "emitDeclarationOnly": true }, "exclude": [ "Gulpfile.ts", diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/WTG3000MfdInstrument.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/WTG3000MfdInstrument.tsx index 4774aee09..abf64cf01 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/WTG3000MfdInstrument.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/WTG3000MfdInstrument.tsx @@ -1,9 +1,3 @@ -import { - APRadioNavInstrument, ArrayUtils, AuralAlertSystem, AuralAlertSystemWarningAdapter, AuralAlertSystemXmlAdapter, CasSystem, CasSystemLegacyAdapter, ClockEvents, - CompositeLogicXMLHost, ControlEvents, DefaultXmlAuralAlertParser, FlightPlanCalculatedEvent, FlightPlannerEvents, FlightTimerInstrument, FlightTimerMode, - FSComponent, GameStateProvider, GpsSynchronizer, GPSSystemState, MinimumsManager, NavComInstrument, NavSourceType, PluginSystem, - SetSubject, SimVarValueType, SoundServer, Subject, TrafficInstrument, UserSetting, Vec2Math, VNode, Wait, XMLWarningFactory, XPDRInstrument -} from '@microsoft/msfs-sdk'; import { ComRadioSpacingManager, DateTimeUserSettings, DefaultGpsIntegrityDataProvider, DefaultRadarAltimeterDataProvider, DefaultVNavDataProvider, DefaultWindDataProvider, DmeUserSettings, FlightPathCalculatorManager, FlightPlanSimSyncManager, Fms, FmsPositionSystemSelector, GarminAPConfig, @@ -11,6 +5,12 @@ import { MinimumsUnitsManager, NavdataComputer, TrafficOperatingModeManager, TrafficOperatingModeSetting, TrafficSystemType, TrafficUserSettings, UnitsUserSettings } from '@microsoft/msfs-garminsdk'; +import { + APRadioNavInstrument, ArrayUtils, AuralAlertSystem, AuralAlertSystemWarningAdapter, AuralAlertSystemXmlAdapter, CasSystem, CasSystemLegacyAdapter, ClockEvents, + CompositeLogicXMLHost, ControlEvents, DefaultXmlAuralAlertParser, FlightPlanCalculatedEvent, FlightPlannerEvents, FlightTimerInstrument, FlightTimerMode, + FSComponent, GameStateProvider, GpsSynchronizer, GPSSystemState, MinimumsManager, NavComInstrument, NavSourceType, PluginSystem, + SetSubject, SimVarValueType, SoundServer, Subject, TrafficInstrument, UserSetting, Vec2Math, VNode, Wait, XMLWarningFactory, XPDRInstrument +} from '@microsoft/msfs-sdk'; import { AuralAlertUserSettings, AuralAlertVoiceSetting, AvionicsConfig, AvionicsStatus, AvionicsStatusChangeEvent, AvionicsStatusEvents, AvionicsStatusGlobalPowerEvent, AvionicsStatusManager, ConnextWeatherPaneView, DisplayPaneContainer, DisplayPaneIndex, DisplayPanesController, DisplayPaneViewFactory, DisplayPaneViewKeys, @@ -26,6 +26,7 @@ import { StartupScreen } from './Components/Startup/StartupScreen'; import { StartupScreenPrebuiltRow, StartupScreenRowFactory } from './Components/Startup/StartupScreenRow'; import { MfdConfig } from './Config/MfdConfig'; import { FmsSpeedManager } from './FmsSpeed/FmsSpeedManager'; +import { G3000MfdPlugin, G3000MfdPluginBinder } from './G3000MFDPlugin'; import { AltimeterBaroKeyEventHandler } from './Input/AltimeterBaroKeyEventHandler'; import { ActiveNavSourceManager } from './Navigation/ActiveNavSourceManager'; import { FmsVSpeedManager } from './Performance/TOLD/FmsVSpeedManager'; @@ -38,7 +39,6 @@ import { Taws } from './TAWS/Taws'; import { TouchdownCalloutModule } from './TAWS/TouchdownCalloutModule'; import { VSpeedBugManager } from './VSpeed/VSpeedBugManager'; import { WeatherRadarManager } from './WeatherRadar/WeatherRadarManager'; -import { G3000MfdPlugin, G3000MfdPluginBinder } from './G3000MFDPlugin'; import './WTG3000_MFD.css'; @@ -126,6 +126,7 @@ export class WTG3000MfdInstrument extends WTG3000FsInstrument { private readonly gps2DataProvider = new GpsStatusDataProvider(this.bus, 2); private readonly apConfig = new GarminAPConfig(this.bus, this.flightPlanner, this.verticalPathCalculator, { + useIndicatedMach: true, vnavOptions: { allowApproachBaroVNav: true, allowPlusVWithoutSbas: true, @@ -286,7 +287,9 @@ export class WTG3000MfdInstrument extends WTG3000FsInstrument { this.initAuralAlertUserSettings(); this.initTouchdownCalloutUserSettings(); - this.doInit(); + this.doInit().catch(e => { + console.error(e); + }); } /** @inheritdoc */ diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/package-defs-only.json b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/package-defs-only.json index 2cd78b971..f810e9f2d 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/package-defs-only.json +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/package-defs-only.json @@ -1,6 +1,6 @@ { "name": "@microsoft/msfs-wtg3000-mfd", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000 MFD Definitions", "typings": "index.d.ts", "repository": { diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/package.json b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/package.json index e14e2cf9b..342a26818 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/package.json +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/package.json @@ -1,18 +1,15 @@ { "name": "@microsoft/msfs-wtg3000-mfd", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000 MFD Definitions", "typings": "index.d.ts", "scripts": { - "build:defs": "tsc & robocopy . *.css build /s & rollup -c rollup-defs.config.js & cp package-defs-only.json dist/package.json & rm dist/ignoreme.css" + "build:defs": "tsc && cp package-defs-only.json build/package.json" }, "repository": { "type": "git", "url": "https://github.com/microsoft/msfs-avionics-mirror/" }, "author": "Working Title Simulations, LLC", - "license": "Modified MIT", - "dependencies": { - "rollup": "^2.79.1" - } -} + "license": "Modified MIT" +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/rollup-defs.config.js b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/rollup-defs.config.js deleted file mode 100644 index 9cb07a430..000000000 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/rollup-defs.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import dts from 'rollup-plugin-dts' -import css from 'rollup-plugin-import-css'; -import resolve from '@rollup/plugin-node-resolve'; - -const config = [ - { - input: 'build/index.d.ts', - output: [{ file: 'dist/index.d.ts', format: 'es' }], - plugins: [dts(), resolve(), css({ output: 'ignoreme.css' })], - } -] -export default config \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/tsconfig.json b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/tsconfig.json index 410204972..1afd04606 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/tsconfig.json +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/MFD/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "build", - "emitDeclarationOnly": false + "emitDeclarationOnly": true }, "exclude": [ "Gulpfile.ts", diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/Components/Airspeed/AirspeedIndicator.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/Components/Airspeed/AirspeedIndicator.tsx index e79145522..a27dc5958 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/Components/Airspeed/AirspeedIndicator.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/Components/Airspeed/AirspeedIndicator.tsx @@ -143,7 +143,6 @@ export class AirspeedIndicator extends DisplayComponent return ( { return 'GP'; case APVerticalModes.PITCH: return 'PIT'; + case APVerticalModes.LEVEL: + return 'LVL'; case APVerticalModes.TO: return 'TO'; case APVerticalModes.GA: diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/Components/Horizon/HorizonDisplay.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/Components/Horizon/HorizonDisplay.tsx index 853f07c5a..94e9602aa 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/Components/Horizon/HorizonDisplay.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/Components/Horizon/HorizonDisplay.tsx @@ -76,11 +76,8 @@ export class HorizonDisplay extends DisplayComponent { /** @inheritdoc */ public render(): VNode { const articialHorizonOptions: ArtificialHorizonOptions = { - groundColor: '#3a2400', - skyColors: [ - [0, '#284be4'], - [50, '#0033e6'] - ] + groundColors: [[0, '#a15f02'], [156, '#54350a']], + skyColors: [[0, '#6182e8'], [156, '#0033ff']] }; const horizonLineOptions: HorizonLineOptions = { diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/WTG3000PfdInstrument.tsx b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/WTG3000PfdInstrument.tsx index 7daf6038e..870252dab 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/WTG3000PfdInstrument.tsx +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/WTG3000PfdInstrument.tsx @@ -2,7 +2,9 @@ import { CasSystem, CompositeLogicXMLHost, FSComponent, PluginSystem, SetSubject import { AdfRadioNavSource, DefaultAltimeterDataProvider, DefaultGpsIntegrityDataProvider, DefaultRadarAltimeterDataProvider, - DefaultVNavDataProvider, DefaultWindDataProvider, Fms, GpsNavSource, NavReferenceIndicatorsCollection, NavRadioNavSource, + DefaultVNavDataProvider, DefaultWindDataProvider, Fms, GpsNavSource, + NavRadioNavSource, + NavReferenceIndicatorsCollection, NavReferenceSource, NavReferenceSourceCollection } from '@microsoft/msfs-garminsdk'; @@ -16,11 +18,11 @@ import { import { PfdInstrumentContainer } from './Components/InstrumentContainer/PfdInstrumentContainer'; import { PfdConfig } from './Config/PfdConfig'; +import { G3000PfdPlugin, G3000PfdPluginBinder } from './G3000PFDPlugin'; import { PfdBaroKnobInputHandler } from './Input/PfdBaroKnobInputHandler'; import { PfdCourseKnobInputHandler } from './Input/PfdCourseKnobInputHandler'; import { PfdMapJoystickInputHandler } from './Input/PfdMapJoystickInputHandler'; import { TrafficAuralAlertManager } from './Traffic/TrafficAuralAlertManager'; -import { G3000PfdPlugin, G3000PfdPluginBinder } from './G3000PFDPlugin'; import './WTG3000_PFD.css'; @@ -165,7 +167,9 @@ export class WTG3000PfdInstrument extends WTG3000FsInstrument { this.backplane.addInstrument(InstrumentBackplaneNames.Traffic, this.trafficInstrument); - this.doInit(); + this.doInit().catch(e => { + console.error(e); + }); } /** @inheritdoc */ diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/package-defs-only.json b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/package-defs-only.json index 644f4dd12..73ff4895c 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/package-defs-only.json +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/package-defs-only.json @@ -1,6 +1,6 @@ { "name": "@microsoft/msfs-wtg3000-pfd", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000 PFD Definitions", "typings": "index.d.ts", "repository": { diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/package.json b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/package.json index 83b98bf94..5553527ab 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/package.json +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/package.json @@ -1,18 +1,15 @@ { "name": "@microsoft/msfs-wtg3000-pfd", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000 PFD Definitions", "typings": "index.d.ts", "scripts": { - "build:defs": "tsc & robocopy . *.css build /s & rollup -c rollup-defs.config.js & cp package-defs-only.json dist/package.json & rm dist/ignoreme.css" + "build:defs": "tsc && cp package-defs-only.json build/package.json" }, "repository": { "type": "git", "url": "https://github.com/microsoft/msfs-avionics-mirror/" }, "author": "Working Title Simulations, LLC", - "license": "Modified MIT", - "dependencies": { - "rollup": "^2.79.1" - } -} + "license": "Modified MIT" +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/rollup-defs.config.js b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/rollup-defs.config.js deleted file mode 100644 index 9cb07a430..000000000 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/rollup-defs.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import dts from 'rollup-plugin-dts' -import css from 'rollup-plugin-import-css'; -import resolve from '@rollup/plugin-node-resolve'; - -const config = [ - { - input: 'build/index.d.ts', - output: [{ file: 'dist/index.d.ts', format: 'es' }], - plugins: [dts(), resolve(), css({ output: 'ignoreme.css' })], - } -] -export default config \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/tsconfig.json b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/tsconfig.json index 410204972..1afd04606 100644 --- a/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/tsconfig.json +++ b/src/workingtitle-instruments-g3000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/PFD/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "build", - "emitDeclarationOnly": false + "emitDeclarationOnly": true }, "exclude": [ "Gulpfile.ts", diff --git a/src/workingtitle-instruments-g3000/package.json b/src/workingtitle-instruments-g3000/package.json index ca43bbd94..f372a9045 100644 --- a/src/workingtitle-instruments-g3000/package.json +++ b/src/workingtitle-instruments-g3000/package.json @@ -1,6 +1,6 @@ { "name": "workingtitle-instruments-g3000", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000", "main": "wtg3000.js", "scripts": { diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Autopilot/G3000Autopilot.ts b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Autopilot/G3000Autopilot.ts index 7fe95dcdc..120411745 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Autopilot/G3000Autopilot.ts +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Autopilot/G3000Autopilot.ts @@ -1,5 +1,5 @@ -import { APConfig, APStateManager, EventBus, FlightPlanner, MetricAltitudeSettingsManager } from '@microsoft/msfs-sdk'; -import { GarminAutopilot, MinimumsDataProvider } from '@microsoft/msfs-garminsdk'; +import { APStateManager, EventBus, FlightPlanner, MetricAltitudeSettingsManager } from '@microsoft/msfs-sdk'; +import { GarminAPConfigInterface, GarminAutopilot, MinimumsDataProvider } from '@microsoft/msfs-garminsdk'; /** * A G3000 autopilot. @@ -18,7 +18,7 @@ export class G3000Autopilot extends GarminAutopilot { constructor( bus: EventBus, flightPlanner: FlightPlanner, - config: APConfig, + config: GarminAPConfigInterface, stateManager: APStateManager, metricAltSettingsManager: MetricAltitudeSettingsManager, minimumsDataProvider: MinimumsDataProvider diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Components/Map/Indicators/MapRelativeTerrainStatusIndicator.css b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Components/Map/Indicators/MapRelativeTerrainStatusIndicator.css index aadeafcee..4b24e432c 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Components/Map/Indicators/MapRelativeTerrainStatusIndicator.css +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Components/Map/Indicators/MapRelativeTerrainStatusIndicator.css @@ -9,6 +9,7 @@ position: relative; width: var(--map-rel-terrain-status-icon-width, 1.3em); height: var(--map-rel-terrain-status-icon-height, 1.2em); + overflow: hidden; } .map-rel-terrain-status-icon { @@ -25,6 +26,7 @@ top: 0px; width: 100%; height: 100%; + overflow: visible; } .map-rel-terrain-status-failed-cross { diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Components/NavigationMap/FlightPlanTextInset/FlightPlanTextRow.tsx b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Components/NavigationMap/FlightPlanTextInset/FlightPlanTextRow.tsx index 4d1ad88d3..9ecf9e8be 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Components/NavigationMap/FlightPlanTextInset/FlightPlanTextRow.tsx +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Components/NavigationMap/FlightPlanTextInset/FlightPlanTextRow.tsx @@ -1,8 +1,8 @@ import { AirportFacility, AltitudeRestrictionType, ApproachProcedure, BasicNavAngleSubject, BasicNavAngleUnit, ComponentProps, - DefaultUserSettingManager, DisplayComponent, DurationDisplay, DurationDisplayDelim, DurationDisplayFormat, EventBus, + DisplayComponent, DurationDisplay, DurationDisplayDelim, DurationDisplayFormat, EventBus, FSComponent, FlightPlanSegmentType, LegType, MappedSubject, NumberFormatter, NumberUnitSubject, SetSubject, - Subject, Subscribable, Subscription, UnitType, VNode, + Subject, Subscribable, Subscription, UnitType, UserSettingManager, VNode, } from '@microsoft/msfs-sdk'; import { @@ -30,7 +30,7 @@ export interface FlightPlanTextRowProps extends ComponentProps { /** The units settings manager. */ unitsSettingManager: UnitsUserSettingManager; /** The date time user settings. */ - dateTimeSettingManager: DefaultUserSettingManager; + dateTimeSettingManager: UserSettingManager; /** The flight plan text data. */ data: Subscribable; /** The selected row. */ diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Config/NumericConfig.ts b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Config/NumericConfig.ts index 4a29f4c81..9e0e335ce 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Config/NumericConfig.ts +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Config/NumericConfig.ts @@ -188,6 +188,8 @@ class ChainedMappedSubscribable implements MappedSubscribable { /** @inheritdoc */ public readonly isSubscribable = true; + /** @inheritdoc */ + public readonly canInitialNotify = true; /** @inheritdoc */ public get isAlive(): boolean { diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Config/SpeedConfig.ts b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Config/SpeedConfig.ts index 9620ef4a7..77337c028 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Config/SpeedConfig.ts +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Config/SpeedConfig.ts @@ -12,6 +12,7 @@ import { VSpeedValueKey } from '../VSpeed/VSpeed'; export enum SpeedConfigType { Ias = 'Ias', Mach = 'Mach', + Tas = 'Tas', Aoa = 'Aoa', Reference = 'Reference' } @@ -19,9 +20,9 @@ export enum SpeedConfigType { /** * A configuration object which defines a factory for an airspeed value presented as knots indicated airspeed. * - * The airspeed value can be defined from a specific indicated airspeed, mach number, or angle-of-attack value, from - * a one-dimensional lookup table of any of the previous value types keyed on pressure altitude, or from an aircraft - * reference speed. + * The airspeed value can be defined from a specific indicated airspeed, mach number, true airspeed, or angle-of-attack + * value, from a one-dimensional lookup table of any of the previous value types keyed on pressure altitude, or from an + * aircraft reference speed. */ export class SpeedConfig implements NumericConfig { public readonly isResolvableConfig = true; @@ -46,6 +47,7 @@ export class SpeedConfig implements NumericConfig { switch (type) { case SpeedConfigType.Ias: case SpeedConfigType.Mach: + case SpeedConfigType.Tas: case SpeedConfigType.Aoa: case SpeedConfigType.Reference: this.type = type; @@ -93,6 +95,8 @@ export class SpeedConfig implements NumericConfig { return this.resolveIas(); case SpeedConfigType.Mach: return this.resolveMach(); + case SpeedConfigType.Tas: + return this.resolveTas(); case SpeedConfigType.Aoa: return this.resolveAoa(); case SpeedConfigType.Reference: @@ -142,6 +146,30 @@ export class SpeedConfig implements NumericConfig { }; } + /** + * Resolves this config as a factory for an airspeed value defined from true airspeed. + * @returns A factory for an airspeed value defined from true airspeed. + */ + private resolveTas(): (context: AirspeedDefinitionContext) => number | MappedSubscribable { + const value = this.value as number | LookupTableConfig; + + return (context: AirspeedDefinitionContext) => { + if (typeof value === 'number') { + return context.tasToIas.map(tasToIas => tasToIas * value); + } else { + const table = value.resolve(); + + return MappedSubject.create( + ([indicatedAlt, tasToIas]): number => { + return table.get(indicatedAlt) * tasToIas; + }, + context.pressureAlt, + context.tasToIas + ); + } + }; + } + /** * Resolves this config as a factory for an airspeed value defined from angle of attack. * @returns A factory for an airspeed value defined from angle of attack. diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/G3000Version.ts b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/G3000Version.ts index 6efb78946..aec862fd1 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/G3000Version.ts +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/G3000Version.ts @@ -3,8 +3,8 @@ */ export class G3000Version { /** The current version string. */ - public static readonly VERSION = 'WT1.1.10'; + public static readonly VERSION = 'WT1.1.14'; /** The release date of the current version, as a UNIX timestamp in milliseconds. */ - public static readonly VERSION_DATE = Date.parse('2023-08-14'); + public static readonly VERSION_DATE = Date.parse('2023-11-13'); } \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/NavReference/G3000NavReference.ts b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/NavReference/G3000NavReference.ts index c81567afd..697e9c910 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/NavReference/G3000NavReference.ts +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/NavReference/G3000NavReference.ts @@ -109,7 +109,8 @@ export class G3000ApproachPreviewDataProvider { rnavTypeFlags: RnavTypeFlags.None, isCircling: false, isVtf: false, - referenceFacility: null + referenceFacility: null, + runway: null }, FmsUtils.approachDetailsEquals); private readonly flightPhase = ConsumerSubject.create>(null, { diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Settings/G3000UserSettingSaveManager.ts b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Settings/G3000UserSettingSaveManager.ts index b4b159fc8..f44a08288 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Settings/G3000UserSettingSaveManager.ts +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Settings/G3000UserSettingSaveManager.ts @@ -87,7 +87,9 @@ export class G3000UserSettingSaveManager extends UserSettingSaveManager { ...PfdUserSettings.getMasterManager(bus).getAllSettings().filter(setting => { return G3000UserSettingSaveManager.PFD_SETTINGS.some(value => setting.definition.name.startsWith(value)); }), - ...MapUserSettings.getMasterManager(bus).getAllSettings(), + ...MapUserSettings.getMasterManager(bus).getAllSettings().filter(setting => { + return !setting.definition.name.startsWith('mapGroundNorthUpActive'); + }), ...MapSettingSyncUserSettings.getManager(bus).getAllSettings(), ...WeatherMapUserSettings.getMasterManager(bus).getAllSettings(), ...ConnextMapUserSettings.getMasterManager(bus).getAllSettings(), diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Settings/MapUserSettings.ts b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Settings/MapUserSettings.ts index 4ec18cd21..c8c25abb1 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Settings/MapUserSettings.ts +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/Settings/MapUserSettings.ts @@ -156,6 +156,7 @@ export class MapUserSettings { ['mapOrientation']: MapOrientationSettingMode.HeadingUp, ['mapAutoNorthUpActive']: true, ['mapAutoNorthUpRangeIndex']: 27, // 1000 NM/2000 km + ['mapGroundNorthUpActive']: false, ['mapDeclutter']: MapDeclutterSettingMode.All, ['mapTerrainMode']: MapTerrainSettingMode.Absolute, ['mapTerrainRangeIndex']: 27, // 1000 NM/2000 km @@ -316,6 +317,7 @@ export class G3000MapUserSettingUtils { 'mapOrientation', 'mapAutoNorthUpActive', 'mapAutoNorthUpRangeIndex', + 'mapGroundNorthUpActive', 'mapDeclutter', 'mapTerrainMode', 'mapTerrainRangeIndex', diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/WTG3000FsInstrument.ts b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/WTG3000FsInstrument.ts index 90aaa7a16..c570a9176 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/WTG3000FsInstrument.ts +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/WTG3000FsInstrument.ts @@ -226,7 +226,13 @@ export abstract class WTG3000FsInstrument implements FsInstrument { 'coui://html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Assets/Data/gps_sbas.json', 5000, Object.values(SBASGroupName), - this.instrumentType === 'MFD' ? 'primary' : 'replica' + this.instrumentType === 'MFD' ? 'primary' : 'replica', + { + channelCount: 15, + satInUseMaxCount: 15, + satInUsePdopTarget: 2, + satInUseOptimumCount: 5 + } ), iau.gpsDefinition.electricity )); diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/package.json b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/package.json index ebeca5c82..330b9f390 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/package.json +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/package.json @@ -1,11 +1,11 @@ { "name": "@microsoft/msfs-wtg3000-common", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000 Common Library", "main": "index.ts", "scripts": { "build:gulp": "esbuild Gulpfile.ts --bundle --outfile=gulpfile.js --platform=node --target=node16 --packages=external", - "build:defs": "tsc & robocopy . *.css build /s & rollup -c rollup-defs.config.js & cp package-defs-only.json dist/package.json & rm dist/ignoreme.css", + "build:defs": "tsc -p tsconfig-defs-only.json && cp package-defs-only.json build/package.json", "build": "gulp build", "copy": "gulp dist", "clean": "gulp clean" @@ -17,11 +17,8 @@ "author": "Working Title Simulations, LLC", "license": "Modified MIT", "devDependencies": { - "@microsoft/msfs-garminsdk": "*", "@microsoft/msfs-sdk": "*", - "@microsoft/msfs-types": "*" - }, - "dependencies": { - "rollup": "^2.79.1" + "@microsoft/msfs-types": "*", + "@microsoft/msfs-garminsdk": "*" } -} +} \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/package.json.src b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/package.json.src index 39a1ac3f4..5c7100477 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/package.json.src +++ b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/package.json.src @@ -1,6 +1,6 @@ { "name": "wtg3000common", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000 Common Library", "main": "wtg3000common.js", "files": ["wtg3000common.js", "wtg3000common.d.ts", "wtg3000common.css"], diff --git a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/rollup-defs.config.js b/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/rollup-defs.config.js deleted file mode 100644 index 9cb07a430..000000000 --- a/src/workingtitle-instruments-g3000/wtg3000common/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG3000/Shared/rollup-defs.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import dts from 'rollup-plugin-dts' -import css from 'rollup-plugin-import-css'; -import resolve from '@rollup/plugin-node-resolve'; - -const config = [ - { - input: 'build/index.d.ts', - output: [{ file: 'dist/index.d.ts', format: 'es' }], - plugins: [dts(), resolve(), css({ output: 'ignoreme.css' })], - } -] -export default config \ No newline at end of file diff --git a/src/workingtitle-instruments-g3000/wtg3000common/package.json b/src/workingtitle-instruments-g3000/wtg3000common/package.json index ac9e4669e..9f3fa99b0 100644 --- a/src/workingtitle-instruments-g3000/wtg3000common/package.json +++ b/src/workingtitle-instruments-g3000/wtg3000common/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/msfs-wtg3000-common", - "version": "1.1.10", + "version": "1.1.14", "description": "Working Title MSFS G3000 Common Library", "main": "index.ts", "scripts": { diff --git a/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/Autopilot/GNSVNavManager.ts b/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/Autopilot/GNSVNavManager.ts index 73410efcb..f872cf2b8 100644 --- a/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/Autopilot/GNSVNavManager.ts +++ b/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/Autopilot/GNSVNavManager.ts @@ -58,7 +58,8 @@ export class GNSVNavManager implements VNavManager { rnavTypeFlags: RnavTypeFlags.None, isCircling: false, isVtf: false, - referenceFacility: null + referenceFacility: null, + runway: null }, FmsUtils.approachDetailsEquals); public options: GarminVNavGuidanceOptions = { diff --git a/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/MainScreen.tsx b/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/MainScreen.tsx index b98097a93..a0711342f 100644 --- a/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/MainScreen.tsx +++ b/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/MainScreen.tsx @@ -185,7 +185,14 @@ export class MainScreen extends DisplayComponent { 'coui://html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/Assets/gps_ephemeris.json', 'coui://html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/Assets/gps_sbas.json', 5000, - this.enabledSbasGroups + this.enabledSbasGroups, + 'none', + { + channelCount: 15, + satInUseMaxCount: 15, + satInUsePdopTarget: 2, + satInUseOptimumCount: 5 + } ); this.adcPublisher = new AdcPublisher(this.props.bus); this.ahrsPublisher = new GnsAhrsPublisher(this.props.bus); @@ -347,6 +354,7 @@ export class MainScreen extends DisplayComponent { private onPowerStateChanged(state: PowerState): void { this.currentPowerState = state; if (state === PowerState.OnSkipInit) { + this.gpsSatComputer.acquireAndUseSatellites(); this.pageContainer.instance.openPageGroup('NAV', true, 1); } diff --git a/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/StartupScreen.tsx b/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/StartupScreen.tsx index 2547d3ffa..968b5831d 100644 --- a/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/StartupScreen.tsx +++ b/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/StartupScreen.tsx @@ -153,7 +153,7 @@ export class StartupScreen extends DisplayComponent { Garmin Ltd. or its subs

- Main SW Version 5.40 - WT 1.1.9
+ Main SW Version 5.40 - WT 1.1.10
GPS SW Version 5.0
@@ -194,4 +194,4 @@ export class StartupScreen extends DisplayComponent {
); } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/UI/DataFields/GNSDataFieldRenderer.tsx b/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/UI/DataFields/GNSDataFieldRenderer.tsx index 0c6b3582b..f319c4b26 100644 --- a/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/UI/DataFields/GNSDataFieldRenderer.tsx +++ b/src/workingtitle-instruments-gns/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/Shared/UI/DataFields/GNSDataFieldRenderer.tsx @@ -1,5 +1,5 @@ import { - ConsumerSubject, DurationDisplay, DurationDisplayDelim, DurationDisplayFormat, FSComponent, NavAngleUnitFamily, NumberFormatter, NumberUnitInterface, + ConsumerSubject, DurationDisplay, DurationDisplayDelim, DurationDisplayFormat, FSComponent, NavAngleUnit, NavAngleUnitFamily, NumberFormatter, NumberUnitInterface, UnitFamily, UserSettingManager, VNode } from '@microsoft/msfs-sdk'; @@ -174,7 +174,7 @@ export class GNSFieldVerticalSpeedRenderer extends GNSDataFieldTypeRenderer { /** @inheritdoc */ - public render(model: NavDataBarFieldModel>): VNode { + public render(model: NavDataBarFieldModel>): VNode { return ( { const airport = this.props.selectedAirport.get(); if (airport !== undefined && this.selectedDeparture !== undefined) { - this.props.fms.insertDeparture(airport, this.selectedDeparture, this.selectedRunway ?? -1, this.selectedTransition ?? -1); + let oneWayRunway: OneWayRunway | undefined = undefined; + if (this.selectedRunway !== undefined) { + const departure = airport.departures[this.selectedDeparture]; + if (departure !== undefined) { + const runwayTransition = departure.runwayTransitions[this.selectedRunway]; + + if (runwayTransition !== undefined) { + oneWayRunway = RunwayUtils.matchOneWayRunway(airport, runwayTransition.runwayNumber, runwayTransition.runwayDesignation); + } + } + + } + + this.props.fms.insertDeparture(airport, this.selectedDeparture, this.selectedRunway ?? -1, this.selectedTransition ?? -1, oneWayRunway); ViewService.back(); ViewService.open('FPL', false, 0); diff --git a/src/workingtitle-instruments-gns/package.json b/src/workingtitle-instruments-gns/package.json index 095aeea13..b7fd8b4f2 100644 --- a/src/workingtitle-instruments-gns/package.json +++ b/src/workingtitle-instruments-gns/package.json @@ -1,6 +1,6 @@ { "name": "workingtitle-instruments-gns", - "version": "1.1.9", + "version": "1.1.10", "description": "Working Title MSFS GNS430/530", "main": "index.ts", "scripts": { @@ -28,4 +28,4 @@ "rollup-plugin-import-css": "^3.1.0", "typescript": "4.5.5" } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/FMC/Pages/ApproachRefPage.ts b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/FMC/Pages/ApproachRefPage.ts index 39b61e0ea..a677e0a89 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/FMC/Pages/ApproachRefPage.ts +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/FMC/Pages/ApproachRefPage.ts @@ -297,8 +297,8 @@ export class ApproachRefPage extends FmcPage { */ private bindVSpeedManualSetting(type: VSpeedType, field: DisplayField): void { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.addBinding(new Binding(this.vspeedSettings.getSettings(type).get('manual')!, (v) => { - if (v === true) { + this.addBinding(new Binding(this.vspeedSettings.getSettings(type).manual, (v) => { + if (v) { field.getOptions().style = '[white]'; this.invalidate(); } @@ -336,9 +336,9 @@ export class ApproachRefPage extends FmcPage { */ private setVSpeedSetting(type: VSpeedType, value: number): void { const setting = this.vspeedSettings.getSettings(type); - setting.get('value')?.set(Math.trunc(value)); - setting.get('manual')?.set(false); - setting.get('show')?.set(true); + setting.value.set(Math.trunc(value)); + setting.manual.set(false); + setting.show.set(true); } /** @inheritDoc */ diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/FMC/Pages/TakeoffRefPage.ts b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/FMC/Pages/TakeoffRefPage.ts index 966297a8b..87eb482bf 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/FMC/Pages/TakeoffRefPage.ts +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/FMC/Pages/TakeoffRefPage.ts @@ -341,8 +341,8 @@ export class TakeoffRefPage extends FmcPage { */ private bindVSpeedManualSetting(type: VSpeedType, field: DisplayField): void { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.addBinding(new Binding(this.vspeedSettings.getSettings(type).get('manual')!, (v) => { - if (v === true) { + this.addBinding(new Binding(this.vspeedSettings.getSettings(type).manual, (v) => { + if (v) { field.getOptions().style = '[white]'; this.invalidate(); } @@ -389,9 +389,9 @@ export class TakeoffRefPage extends FmcPage { */ private setVSpeedSetting(type: VSpeedType, value: number): void { const setting = this.vspeedSettings.getSettings(type); - setting.get('value')?.set(Math.trunc(value)); - setting.get('manual')?.set(false); - setting.get('show')?.set(true); + setting.value.set(Math.trunc(value)); + setting.manual.set(false); + setting.show.set(true); } /** @inheritDoc */ diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/MFDUserSettings.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/MFDUserSettings.tsx index 874e8201a..cf7238794 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/MFDUserSettings.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/MFDUserSettings.tsx @@ -100,6 +100,14 @@ const mfdSettings = [ name: 'mfdSelectedTextPage_2', defaultValue: WT21MfdTextPage.TakeoffRef as WT21MfdTextPage, }, + { + name: 'mfdSoftkeyFormatChangeActive_1', + defaultValue: false as boolean, + }, + { + name: 'mfdSoftkeyFormatChangeActive_2', + defaultValue: false as boolean, + }, { name: 'memButton1_1', defaultValue: JSON.stringify(mem1defaultValue), @@ -147,6 +155,10 @@ const mfdSettingsAliased = [ name: 'mfdSelectedTextPage', defaultValue: WT21MfdTextPage.TakeoffRef as WT21MfdTextPage, }, + { + name: 'mfdSoftkeyFormatChangeActive', + defaultValue: false as boolean, + }, { name: 'memButton1', defaultValue: JSON.stringify(mem1defaultValue), @@ -203,6 +215,7 @@ export class MFDUserSettings { mfdUpperFmsTextVNavShow: `mfdUpperFmsTextVNavShow_${index}`, mfdDisplayMode: `mfdDisplayMode_${index}`, mfdSelectedTextPage: `mfdSelectedTextPage_${index}`, + mfdSoftkeyFormatChangeActive: `mfdSoftkeyFormatChangeActive_${index}`, memButton1: `memButton1_${index}`, memButton2: `memButton2_${index}`, memButton3: `memButton3_${index}`, diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/Menus/MfdLwrMenuViewService.ts b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/Menus/MfdLwrMenuViewService.ts index 2722812ef..a50b62e89 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/Menus/MfdLwrMenuViewService.ts +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/Menus/MfdLwrMenuViewService.ts @@ -1,13 +1,32 @@ +import { EventBus } from '@microsoft/msfs-sdk'; + +import { DisplayUnitConfigInterface, DisplayUnitLayout } from '../../Shared/Config/DisplayUnitConfig'; +import { MapUserSettings } from '../../Shared/Map/MapUserSettings'; import { GuiHEvent } from '../../Shared/UI/GuiHEvent'; import { MenuViewService } from '../../Shared/UI/MenuViewService'; +import { CcpEvent } from '../CCP/CcpEvent'; +import { CcpEventPublisherType } from '../CCP/CcpEventPublisher'; +import { MFDUpperWindowState, MFDUserSettings } from '../MFDUserSettings'; import { MfdLwrMenu } from './MfdLwrMenu'; -const WT21_H_EVENT_MFD_REGEX = /Generic_Lwr_([12])_(.*)/; +/** + * MFD lower view keys + */ +export interface MfdLwrMenuViewKeys { + /** MFD lower menu */ + 'MfdLwrMenu': void, + + /** MFD lower overlays menu */ + 'MfdLwrOverlaysMenu': void, + + /** MFD lower map symbols menu */ + 'MfdLwrMapSymbolsMenu': void, +} /** * A service to manage mfd lower menu views. */ -export class MfdLwrMenuViewService extends MenuViewService { +export class MfdLwrMenuViewService extends MenuViewService { protected readonly guiEventMap: Map = new Map([ ['MENU_ADV_INC', GuiHEvent.LOWER_INC], ['MENU_ADV_DEC', GuiHEvent.LOWER_DEC], @@ -15,38 +34,156 @@ export class MfdLwrMenuViewService extends MenuViewService { ['DATA_DEC', GuiHEvent.UPPER_DEC], ['DATA_PUSH', GuiHEvent.UPPER_PUSH], ['Push_LWR_MENU', GuiHEvent.LWR_MENU_PUSH], - ['Push_ESC', GuiHEvent.MFD_ESC] + ['Push_ESC', GuiHEvent.MFD_ESC], + ['Push_1L', GuiHEvent.SOFTKEY_1L], + ['Push_2L', GuiHEvent.SOFTKEY_2L], + ['Push_3L', GuiHEvent.SOFTKEY_3L], + ['Push_4L', GuiHEvent.SOFTKEY_4L], + ['Push_1R', GuiHEvent.SOFTKEY_1R], + ['Push_2R', GuiHEvent.SOFTKEY_2R], + ['Push_3R', GuiHEvent.SOFTKEY_3R], + ['Push_4R', GuiHEvent.SOFTKEY_4R], ]); - /** MfdLwrMenuViewService constructor + private readonly mapSettingsManagerMfd = MapUserSettings.getAliasedManager(this.bus, 'MFD'); + private readonly userSettingsManagerMfd = MFDUserSettings.getAliasedManager(this.bus); + + private readonly mfdSoftkeyFormatChangeActiveSetting = this.userSettingsManagerMfd.getSetting('mfdSoftkeyFormatChangeActive'); + private readonly mfdLowerFormatSetting = this.mapSettingsManagerMfd.getSetting('hsiFormat'); + private readonly mfdUpperFormatSetting = this.userSettingsManagerMfd.getSetting('mfdUpperWindowState'); + + private mfdSoftkeyFormatChangeActiveTimeout: NodeJS.Timer | undefined = undefined; + + /** + * MfdLwrMenuViewService constructor + * @param bus the event bus + * @param displayUnitConfig the display unit config * @param otherMenuServices Other menus on this screen that should be closed when this menu is opened */ - public constructor(public readonly otherMenuServices: MenuViewService[] = []) { + public constructor( + private readonly bus: EventBus, + private readonly displayUnitConfig: DisplayUnitConfigInterface, + public readonly otherMenuServices: MenuViewService[] = [], + ) { super(); } + private readonly isUsingSoftkeys = this.displayUnitConfig.displayUnitLayout === DisplayUnitLayout.Softkeys; + /** @inheritdoc */ public onInteractionEvent(hEvent: string, instrumentIndex: number): boolean { - const hEventWithoutPrefix = WT21_H_EVENT_MFD_REGEX[Symbol.match](hEvent); - if (hEventWithoutPrefix !== null) { - const evtIndex = hEventWithoutPrefix[1]; - if (Number(evtIndex) === instrumentIndex) { - const evt = this.guiEventMap.get(hEventWithoutPrefix[2]); - if (evt !== undefined) { - this.startInteractionTimeout(); - if (evt === GuiHEvent.LWR_MENU_PUSH) { - this.otherMenuServices.forEach(x => x.activeView.get()?.close()); - (this.activeView.get() instanceof MfdLwrMenu) ? this.activeView.get()?.close() : this.open('MfdLwrMenu'); - return true; - } else if (evt == GuiHEvent.MFD_ESC) { - const isInSubmenu = (this.activeView.get() && !(this.activeView.get() instanceof MfdLwrMenu)); - this.activeView.get()?.close(); - if (isInSubmenu) { - this.open('MfdLwrMenu'); + if (hEvent.startsWith('Generic_Lwr_')) { + const hEventWithoutPrefix = /Generic_Lwr_([12])_(.*)/[Symbol.match](hEvent); + + if (hEventWithoutPrefix !== null) { + const evtIndex = hEventWithoutPrefix[1]; + + if (Number(evtIndex) === instrumentIndex) { + const evt = this.guiEventMap.get(hEventWithoutPrefix[2]); + + if (evt !== undefined) { + this.startInteractionTimeout(); + + if (evt === GuiHEvent.LWR_MENU_PUSH) { + this.otherMenuServices.forEach(x => x.activeView.get()?.close()); + (this.activeView.get() instanceof MfdLwrMenu) ? this.activeView.get()?.close() : this.open('MfdLwrMenu'); + return true; + } else if (evt == GuiHEvent.MFD_ESC) { + const isInSubmenu = (this.activeView.get() && !(this.activeView.get() instanceof MfdLwrMenu)); + this.activeView.get()?.close(); + if (isInSubmenu) { + this.open('MfdLwrMenu'); + } + return true; + } else { + return this.routeInteractionEventToViews(evt); + } + } + } + } + } else if (this.isUsingSoftkeys && hEvent.startsWith('Generic_Display_')) { + const hEventWithoutPrefix = /Generic_Display_MFD([12])_(.*)/[Symbol.match](hEvent); + + if (hEventWithoutPrefix !== null) { + const evtIndex = hEventWithoutPrefix[1]; + + if (Number(evtIndex) === instrumentIndex) { + const evt = this.guiEventMap.get(hEventWithoutPrefix[2]); + + if (evt !== undefined) { + this.startInteractionTimeout(); + + switch (evt) { + case GuiHEvent.SOFTKEY_1L: { + if (this.mfdSoftkeyFormatChangeActiveTimeout !== undefined) { + clearTimeout(this.mfdSoftkeyFormatChangeActiveTimeout); + } + + this.mfdSoftkeyFormatChangeActiveTimeout = setTimeout(() => this.mfdSoftkeyFormatChangeActiveSetting.set(false), 10_000); + + if (!this.mfdSoftkeyFormatChangeActiveSetting.get()) { + this.mfdSoftkeyFormatChangeActiveSetting.set(true); + return true; + } + + const currentFormat = this.mfdUpperFormatSetting.get(); + + let nextFormat; + switch (currentFormat) { + default: + case MFDUpperWindowState.Off: + nextFormat = MFDUpperWindowState.FmsText; + break; + case MFDUpperWindowState.FmsText: + nextFormat = MFDUpperWindowState.Checklist; + break; + // case MFDUpperWindowState.Checklist: nextFormatIndex = MFDUpperWindowState.PassBrief; break; + case MFDUpperWindowState.Checklist: + nextFormat = MFDUpperWindowState.Systems; + break; + } + + this.mfdUpperFormatSetting.set(nextFormat); + break; + } + case GuiHEvent.SOFTKEY_1R: { + if (this.mfdSoftkeyFormatChangeActiveTimeout !== undefined) { + clearTimeout(this.mfdSoftkeyFormatChangeActiveTimeout); + } + + this.mfdSoftkeyFormatChangeActiveTimeout = setTimeout(() => this.mfdSoftkeyFormatChangeActiveSetting.set(false), 10_000); + + if (!this.mfdSoftkeyFormatChangeActiveSetting.get()) { + this.mfdSoftkeyFormatChangeActiveSetting.set(true); + return true; + } + + const lowerFormats = MapUserSettings.hsiFormatsMFD; + const currentFormatIndex = lowerFormats.indexOf(this.mfdLowerFormatSetting.get()); + const nextFormatIndex = (currentFormatIndex + 1) % (lowerFormats.length - 1); + + this.mfdLowerFormatSetting.set(lowerFormats[nextFormatIndex]); + break; + } + case GuiHEvent.SOFTKEY_2R: { + // OVERLAYS + // TODO this is not super great - ideally, the HSI container would be a view, and it would transmit events to components. + // But this requires a larger refactor. + // For now, we transmit the corresponding DCP event instead. + this.bus.getPublisher().pub('ccpEvent', CcpEvent.CCP_TERR_WX); + return true; + } + case GuiHEvent.SOFTKEY_3R: { + // TFC + // TODO this is not super great - ideally, the HSI container would be a view, and it would transmit events to components. + // But this requires a larger refactor. + // For now, we transmit the corresponding DCP event instead. + this.bus.getPublisher().pub('ccpEvent', CcpEvent.CCP_TFC); + return false; + } + default: + return this.routeInteractionEventToViews(evt); } - return true; - } else { - return this.routeInteractionEventToViews(evt); } } } diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/Menus/MfdUprMenuViewService.ts b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/Menus/MfdUprMenuViewService.ts index 6ca973aac..b929a9782 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/Menus/MfdUprMenuViewService.ts +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/Menus/MfdUprMenuViewService.ts @@ -4,10 +4,18 @@ import { MfdUprMenu } from './MfdUprMenu'; const WT21_H_EVENT_MFD_REGEX = /Generic_Lwr_([12])_(.*)/; +/** + * MFD upper view keys + */ +export interface MfdUprMenuViewKeys { + /** MFD upper menu */ + 'MfdUprMenu': void, +} + /** * A service to manage mfd upper menu views. */ -export class MfdUprMenuViewService extends MenuViewService { +export class MfdUprMenuViewService extends MenuViewService { protected readonly guiEventMap: Map = new Map([ ['MENU_ADV_INC', GuiHEvent.LOWER_INC], ['MENU_ADV_DEC', GuiHEvent.LOWER_DEC], diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/WT21_MFD_Instrument.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/WT21_MFD_Instrument.tsx index a25748522..19d3be4a5 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/WT21_MFD_Instrument.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/MFD/WT21_MFD_Instrument.tsx @@ -4,19 +4,10 @@ /// import { - AdcPublisher, AhrsPublisher, AutopilotInstrument, BaseInstrumentPublisher, BasicAvionicsSystem, Clock, - ClockEvents, CompositeLogicXMLHost, ConsumerSubject, - DebounceTimer, - ElectricalEvents, EventBus, FacilityLoader, FacilityRepository, FlightPathAirplaneSpeedMode, FlightPathCalculator, FlightPlanner, - FlightPlanPredictor, FSComponent, - FsInstrument, - GNSSPublisher, HEventPublisher, InstrumentBackplane, InstrumentEvents, LNavSimVarPublisher, MinimumsSimVarPublisher, NavComSimVarPublisher, - PressurizationPublisher, SimVarValueType, - SoundServer, - StallWarningEvents, - StallWarningPublisher, - Subject, TrafficInstrument, UserSettingSaveManager, - VNavSimVarPublisher, Wait, XPDRSimVarPublisher, + AdcPublisher, AhrsPublisher, AutopilotInstrument, BaseInstrumentPublisher, BasicAvionicsSystem, Clock, ClockEvents, CompositeLogicXMLHost, ConsumerSubject, + DebounceTimer, ElectricalEvents, EventBus, FacilityLoader, FacilityRepository, FlightPathAirplaneSpeedMode, FlightPathCalculator, FlightPlanner, FlightPlanPredictor, FSComponent, + GNSSPublisher, HEventPublisher, InstrumentBackplane, InstrumentEvents, LNavSimVarPublisher, MinimumsSimVarPublisher, NavComSimVarPublisher, PressurizationPublisher, + SimVarValueType, SoundServer, StallWarningEvents, StallWarningPublisher, Subject, TrafficInstrument, UserSettingSaveManager, VNavSimVarPublisher, Wait, XPDRSimVarPublisher, } from '@microsoft/msfs-sdk'; import { WT21LNavDataEvents, WT21LNavDataSimVarPublisher } from '../FMC/Autopilot/WT21LNavDataEvents'; @@ -51,17 +42,18 @@ import { MfdLwrMapSymbolsMenu } from './Menus/MfdLwrMapSymbolsMenu'; import { WT21FlightPlanPredictorConfiguration } from '../Shared/WT21FlightPlanPredictorConfiguration'; import { WT21Fms } from '../Shared/FlightPlan/WT21Fms'; import { WT21ElectricalSetup } from '../Shared/Systems/WT21ElectricalSetup'; - -import '../Shared/WT21_Common.css'; -import './WT21_MFD.css'; import { WT21XmlAuralsConfig } from '../Shared/WT21XmlAuralsConfig'; import { WT21FixInfoManager } from '../FMC/Systems/WT21FixInfoManager'; import { WT21FixInfoConfig } from '../FMC/Systems/WT21FixInfoConfig'; +import { WT21DisplayUnitFsInstrument, WT21DisplayUnitType } from '../Shared/WT21DisplayUnitFsInstrument'; + +import '../Shared/WT21_Common.css'; +import './WT21_MFD.css'; /** * The WT21 MFD Instrument */ -export class WT21_MFD_Instrument implements FsInstrument { +export class WT21_MFD_Instrument extends WT21DisplayUnitFsInstrument { private readonly backplane: InstrumentBackplane; private readonly baseInstrumentPublisher: BaseInstrumentPublisher; private readonly hEventPublisher: HEventPublisher; @@ -121,6 +113,8 @@ export class WT21_MFD_Instrument implements FsInstrument { * @param instrument The base instrument. */ constructor(readonly instrument: BaseInstrument) { + super(instrument, WT21DisplayUnitType.Mfd, instrument.instrumentIndex === 1 ? 1 : 2); + RegisterViewListener('JS_LISTENER_INSTRUMENTS'); this.bus = new EventBus(); @@ -223,7 +217,7 @@ export class WT21_MFD_Instrument implements FsInstrument { // TODO Deduplicate this code against PFD code this.navIndicators = new NavIndicators(new Map([ - ['courseNeedle', new WT21CourseNeedleNavIndicator(this.navSources, this.bus, 'MFD')], + ['courseNeedle', new WT21CourseNeedleNavIndicator(this.navSources, this, this.bus)], ['bearingPointer1', new WT21BearingPointerNavIndicator(this.navSources, this.bus, 1, 'NAV1')], ['bearingPointer2', new WT21BearingPointerNavIndicator(this.navSources, this.bus, 2, 'NAV2')], ])); @@ -251,7 +245,7 @@ export class WT21_MFD_Instrument implements FsInstrument { this.fixInfoManager = new WT21FixInfoManager(this.bus, this.facLoader, WT21Fms.PRIMARY_ACT_PLAN_INDEX, this.planner, WT21FixInfoConfig /*, this.activeRoutePredictor*/); this.uprMenuViewService = new MfdUprMenuViewService(); - this.lwrMenuViewService = new MfdLwrMenuViewService(); + this.lwrMenuViewService = new MfdLwrMenuViewService(this.bus, this.displayUnitConfig); this.uprMenuViewService.otherMenuServices.push(this.lwrMenuViewService); this.lwrMenuViewService.otherMenuServices.push(this.uprMenuViewService); @@ -288,8 +282,14 @@ export class WT21_MFD_Instrument implements FsInstrument { FSComponent.render( , document.getElementById('HSIMap') diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Components/FlightInstruments/AirspeedIndicator.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Components/FlightInstruments/AirspeedIndicator.tsx index 957fb7eea..56e11213a 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Components/FlightInstruments/AirspeedIndicator.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Components/FlightInstruments/AirspeedIndicator.tsx @@ -295,19 +295,19 @@ export class AirspeedIndicator extends DisplayComponent const type = (vspeed as VSpeedType); const setting = this.vspeedSettings.getSettings(type); - setting.get('value')?.sub((v) => { + setting.value.sub((v) => { this.SpeedBugsMap.get(type)?.instance.setBugSpeed(v as number); this.SpeedValuesMap.get(type)?.instance.setVSpeed(v as number); this.updateAllBugsAndRanges(Simplane.getIndicatedSpeed()); }, true); - setting.get('manual')?.sub((v) => { + setting.manual.sub((v) => { this.SpeedBugsMap.get(type)?.instance.setIsModified(v as boolean); this.SpeedValuesMap.get(type)?.instance.setIsModified(v as boolean); this.updateAllBugsAndRanges(Simplane.getIndicatedSpeed()); }, true); - setting.get('show')?.sub((v) => { + setting.show.sub((v) => { this.SpeedBugsMap.get(type)?.instance.setIsVisible(v as boolean); this.SpeedValuesMap.get(type)?.instance.setIsVisible(v as boolean); this.updateAllBugsAndRanges(Simplane.getIndicatedSpeed()); @@ -335,8 +335,8 @@ export class AirspeedIndicator extends DisplayComponent */ private setVSpeedState = (type: VSpeedType, enabled: boolean): void => { const setting = this.vspeedSettings.getSettings(type); - setting.get('show')?.set(enabled); - setting.get('manual')?.set(true); + setting.show.set(enabled); + setting.manual.set(true); }; diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdMenu.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdMenu.tsx index 55d5b7fda..fbbf2ea9f 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdMenu.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdMenu.tsx @@ -26,9 +26,9 @@ export class PfdMenu extends GuiDialog { private readonly rangeOption = Subject.create(0); private readonly mapSettingsManager = MapUserSettings.getAliasedManager(this.props.bus, 'PFD'); - public readonly navSourcesOrder = ['FMS1', 'NAV1', 'NAV2'] as const; - public readonly navSources = ArraySubject.create(['FMS1', 'VOR1', 'VOR2']); - public readonly ranges = ArraySubject.create(['5', '10', '25', '50', '100', '200', '300', '600']); + private readonly navSourcesOrder = ['FMS1', 'NAV1', 'NAV2'] as const; + private readonly navSources = ArraySubject.create(['FMS1', 'VOR1', 'VOR2']); + private readonly ranges = ArraySubject.create(['5', '10', '25', '50', '100', '200', '300', '600']); /** @inheritdoc */ public onAfterRender(node: VNode): void { diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdMenuViewService.ts b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdMenuViewService.ts index 14e7c5025..80399736d 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdMenuViewService.ts +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdMenuViewService.ts @@ -1,12 +1,84 @@ +import { EventBus } from '@microsoft/msfs-sdk'; + +import { DisplayUnitConfigInterface, DisplayUnitLayout } from '../../Shared/Config/DisplayUnitConfig'; import { GuiHEvent } from '../../Shared/UI/GuiHEvent'; import { MenuViewService } from '../../Shared/UI/MenuViewService'; import { PfdMenu } from './PfdMenu'; import { PfdRefsMenu } from './PfdRefsMenu'; +import { PfdSideButtonsRefs2Menu } from './PfdSideButtonsRefs2Menu'; +import { PfdSideButtonsRefs1Menu } from './PfdSideButtonsRefs1Menu'; +import { DcpEventPublisherType } from '../DCP/DcpEventPublisher'; +import { DcpEvent } from '../DCP/DcpEvent'; +import { WT21CourseNeedleNavIndicator } from '../../Shared/Navigation/WT21NavIndicators'; + +/** + * PFD view keys + */ +export interface PfdMenuViewKeys { + /** PFD menu */ + 'PfdMenu': void, + + /** BRG SRC menu */ + 'BrgSrcMenu': void, + + /** PFD config menu */ + 'PfdConfigMenu': void, + + /** PFD refs menu */ + 'PfdRefsMenu': void, + + /** PFD overlays menu */ + 'PfdOverlaysMenu': void, + + /** PFD baro set menu */ + 'PfdBaroSetMenu': void, + + /** PFD (side button layout) NAV/BRG SRC menu */ + 'PfdSideButtonsNavBrgSrcMenu': void, + + /** PFD (side button layout) REFS 1/2 menu */ + 'PfdSideButtonsRefs1Menu': void, + + /** PFD (side button layout) REFS 2/2 menu */ + 'PfdSideButtonsRefs2Menu': void, +} /** * A service to manage pfd menu views. */ -export class PfdMenuViewService extends MenuViewService { +export class PfdMenuViewService extends MenuViewService { + /** + * Ctor + * @param bus the event bus + * @param displayUnitConfig the display unit config for the PFD + * @param courseNeedleNavIndicator the course needle nav indicator, used to perform nav swap on PRESET LSK press + */ + constructor( + private readonly bus: EventBus, + private readonly displayUnitConfig: DisplayUnitConfigInterface, + private readonly courseNeedleNavIndicator: WT21CourseNeedleNavIndicator, + ) { + super(); + + const dcpSubscriber = bus.getSubscriber(); + + if (displayUnitConfig.displayUnitLayout === DisplayUnitLayout.Softkeys) { + dcpSubscriber.on('dcpEvent').handle((event) => { + if (event === DcpEvent.DCP_NAV) { + const activeView = this.activeViewKey.get(); + + if (activeView === 'PfdSideButtonsNavBrgSrcMenu') { + this.activeView.get()?.close(); + } else { + this.open('PfdSideButtonsNavBrgSrcMenu'); + } + } + }); + } + } + + private readonly isUsingSoftkeys = this.displayUnitConfig.displayUnitLayout === DisplayUnitLayout.Softkeys; + protected readonly guiEventMap: Map = new Map([ ['MENU_ADV_INC', GuiHEvent.LOWER_INC], ['MENU_ADV_DEC', GuiHEvent.LOWER_DEC], @@ -15,35 +87,125 @@ export class PfdMenuViewService extends MenuViewService { ['DATA_PUSH', GuiHEvent.UPPER_PUSH], ['Push_PFD_MENU', GuiHEvent.PFD_MENU_PUSH], ['Push_REFS_MENU', GuiHEvent.REFS_MENU_PUSH], - ['Push_ESC', GuiHEvent.PFD_ESC] + ['Push_ESC', GuiHEvent.PFD_ESC], + ['Push_1L', GuiHEvent.SOFTKEY_1L], + ['Push_2L', GuiHEvent.SOFTKEY_2L], + ['Push_3L', GuiHEvent.SOFTKEY_3L], + ['Push_4L', GuiHEvent.SOFTKEY_4L], + ['Push_1R', GuiHEvent.SOFTKEY_1R], + ['Push_2R', GuiHEvent.SOFTKEY_2R], + ['Push_3R', GuiHEvent.SOFTKEY_3R], + ['Push_4R', GuiHEvent.SOFTKEY_4R], ]); /** @inheritdoc */ public onInteractionEvent(hEvent: string): boolean { - const evt = this.guiEventMap.get(hEvent.replace(/Generic_Upr_([12])_/, '')); - - if (evt !== undefined) { - this.startInteractionTimeout(); - if (evt === GuiHEvent.PFD_MENU_PUSH) { - (this.activeView.get() instanceof PfdMenu) ? this.activeView.get()?.close() : this.open('PfdMenu'); - return true; - } else if (evt == GuiHEvent.PFD_ESC) { - const isInSubmenu = (this.activeView.get() && !(this.activeView.get() instanceof PfdMenu)); - this.activeView.get()?.close(); - if (isInSubmenu) { - this.open('PfdMenu'); - } - - return true; - } else if (evt === GuiHEvent.REFS_MENU_PUSH) { - // TODO check if Refs menu is already open - (this.activeView.get() instanceof PfdRefsMenu) ? this.activeView.get()?.close() : this.open('PfdRefsMenu'); - return true; - } else { + const activeView = this.activeView.get(); + + if (hEvent.startsWith('Generic_Upr')) { + const evt = this.guiEventMap.get(hEvent.replace(/Generic_Upr_([12])_/, '')); + + if (evt !== undefined) { + this.startInteractionTimeout(); + + switch (evt) { + case GuiHEvent.PFD_MENU_PUSH: { + (activeView instanceof PfdMenu) ? activeView?.close() : this.open('PfdMenu'); + return true; + } + case GuiHEvent.PFD_ESC: { + const isInSubmenu = (activeView && !(activeView instanceof PfdMenu)); + + activeView?.close(); + + if (isInSubmenu) { + this.open('PfdMenu'); + } + + return true; + } + case GuiHEvent.REFS_MENU_PUSH: { + // TODO check if Refs menu is already open + if (this.isUsingSoftkeys) { + if (activeView instanceof PfdSideButtonsRefs1Menu) { + this.open('PfdSideButtonsRefs2Menu'); + } else if (activeView instanceof PfdSideButtonsRefs2Menu) { + activeView.close(); + } else { + this.open('PfdSideButtonsRefs1Menu'); + return true; + } + return true; + } else { + (activeView instanceof PfdRefsMenu) ? activeView?.close() : this.open('PfdRefsMenu'); + return true; + } + } + default: + return this.routeInteractionEventToViews(evt); + } + } + + return false; + } else if (this.isUsingSoftkeys && hEvent.startsWith('Generic_Display_')) { + const evt = this.guiEventMap.get(hEvent.replace(/Generic_Display_PFD([12])_/, '')) as GuiHEvent; + + if (activeView) { return this.routeInteractionEventToViews(evt); } + + switch (evt) { + case GuiHEvent.SOFTKEY_1L: { + // noop + return false; + } + case GuiHEvent.SOFTKEY_2L: { + // PRESET TODO noop + this.courseNeedleNavIndicator.navSwap(); + return false; + } + case GuiHEvent.SOFTKEY_3L: { + // noop + return false; + } + case GuiHEvent.SOFTKEY_4L: { + // ET + this.bus.getPublisher().pub('dcpEvent', DcpEvent.DCP_ET); + return false; + } + case GuiHEvent.SOFTKEY_1R: { + // FORMAT + // TODO this is not super great - ideally, the HSI container would be a view, and it would transmit events to components. + // But this requires a larger refactor. + // For now, we transmit the corresponding DCP event instead. + this.bus.getPublisher().pub('dcpEvent', DcpEvent.DCP_FRMT); + return true; + } + case GuiHEvent.SOFTKEY_2R: { + // OVERLAYS + // TODO this is not super great - ideally, the HSI container would be a view, and it would transmit events to components. + // But this requires a larger refactor. + // For now, we transmit the corresponding DCP event instead. + this.bus.getPublisher().pub('dcpEvent', DcpEvent.DCP_TERR_WX); + return true; + } + case GuiHEvent.SOFTKEY_3R: { + // TFC + // TODO this is not super great - ideally, the HSI container would be a view, and it would transmit events to components. + // But this requires a larger refactor. + // For now, we transmit the corresponding DCP event instead. + this.bus.getPublisher().pub('dcpEvent', DcpEvent.DCP_TFC); + return false; + } + case GuiHEvent.SOFTKEY_4R: { + // ??? TODO noop + return false; + } + } + + return false; } return false; } -} \ No newline at end of file +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdRefsMenu.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdRefsMenu.tsx index 473a76804..aadb324e6 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdRefsMenu.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdRefsMenu.tsx @@ -42,21 +42,21 @@ export class PfdRefsMenu extends GuiDialog { ]); private readonly VSpeedValues = new Map>([ - [VSpeedType.V1, this.vSpeedSettings.getSettings(VSpeedType.V1).get('value')! as MutableSubscribable], - [VSpeedType.V2, this.vSpeedSettings.getSettings(VSpeedType.V2).get('value')! as MutableSubscribable], - [VSpeedType.Vr, this.vSpeedSettings.getSettings(VSpeedType.Vr).get('value')! as MutableSubscribable], - [VSpeedType.Venr, this.vSpeedSettings.getSettings(VSpeedType.Venr).get('value')! as MutableSubscribable], - [VSpeedType.Vapp, this.vSpeedSettings.getSettings(VSpeedType.Vapp).get('value')! as MutableSubscribable], - [VSpeedType.Vref, this.vSpeedSettings.getSettings(VSpeedType.Vref).get('value')! as MutableSubscribable], + [VSpeedType.V1, this.vSpeedSettings.getSettings(VSpeedType.V1).value as MutableSubscribable], + [VSpeedType.V2, this.vSpeedSettings.getSettings(VSpeedType.V2).value as MutableSubscribable], + [VSpeedType.Vr, this.vSpeedSettings.getSettings(VSpeedType.Vr).value as MutableSubscribable], + [VSpeedType.Venr, this.vSpeedSettings.getSettings(VSpeedType.Venr).value as MutableSubscribable], + [VSpeedType.Vapp, this.vSpeedSettings.getSettings(VSpeedType.Vapp).value as MutableSubscribable], + [VSpeedType.Vref, this.vSpeedSettings.getSettings(VSpeedType.Vref).value as MutableSubscribable], ]); private readonly VSpeedStates = new Map>([ - [VSpeedType.V1, this.vSpeedSettings.getSettings(VSpeedType.V1).get('show')! as MutableSubscribable], - [VSpeedType.V2, this.vSpeedSettings.getSettings(VSpeedType.V2).get('show')! as MutableSubscribable], - [VSpeedType.Vr, this.vSpeedSettings.getSettings(VSpeedType.Vr).get('show')! as MutableSubscribable], - [VSpeedType.Venr, this.vSpeedSettings.getSettings(VSpeedType.Venr).get('show')! as MutableSubscribable], - [VSpeedType.Vapp, this.vSpeedSettings.getSettings(VSpeedType.Vapp).get('show')! as MutableSubscribable], - [VSpeedType.Vref, this.vSpeedSettings.getSettings(VSpeedType.Vref).get('show')! as MutableSubscribable], + [VSpeedType.V1, this.vSpeedSettings.getSettings(VSpeedType.V1).show as MutableSubscribable], + [VSpeedType.V2, this.vSpeedSettings.getSettings(VSpeedType.V2).show as MutableSubscribable], + [VSpeedType.Vr, this.vSpeedSettings.getSettings(VSpeedType.Vr).show as MutableSubscribable], + [VSpeedType.Venr, this.vSpeedSettings.getSettings(VSpeedType.Venr).show as MutableSubscribable], + [VSpeedType.Vapp, this.vSpeedSettings.getSettings(VSpeedType.Vapp).show as MutableSubscribable], + [VSpeedType.Vref, this.vSpeedSettings.getSettings(VSpeedType.Vref).show as MutableSubscribable], ]); private readonly VSpeedCssClasses = new Map>([ @@ -89,10 +89,10 @@ export class PfdRefsMenu extends GuiDialog { for (const [type, uiRef] of this.VSpeedUiRefs.entries()) { const settings = this.vSpeedSettings.getSettings(type); uiRef.instance.props.onValueChanged = () => { - settings.get('manual')?.set(true); + settings.manual.set(true); }; - settings.get('manual')?.sub((v) => { - this.VSpeedCssClasses.get(type)?.set((v as boolean) === true ? '' : 'magenta'); + settings.manual.sub((v) => { + this.VSpeedCssClasses.get(type)?.set((v as boolean) ? '' : 'magenta'); }, true); } @@ -178,22 +178,6 @@ export class PfdRefsMenu extends GuiDialog { } } - - /** - * A callback called when a VSpeed value needs to be published - * @param type The VSpeed type. - * @param value The VSpeed value. - * @param enabled The VSpeed state. - */ - private publishVSpeed = (type: VSpeedType, value: number, enabled: boolean): void => { - // const publisher = this.props.bus.getPublisher(); - // publisher.pub('vspeed', { type: type, value: value, enabled: enabled, modified: true }, true, true); - const setting = this.vSpeedSettings.getSettings(type); - setting.get('value')?.set(value); - setting.get('manual')?.set(value); - setting.get('show')?.set(enabled); - }; - /** @inheritdoc */ public render(): VNode { return ( diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsNavBrgSrcMenu.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsNavBrgSrcMenu.css new file mode 100644 index 000000000..ceb3485c8 --- /dev/null +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsNavBrgSrcMenu.css @@ -0,0 +1,32 @@ +.pfd-popup-nav-src-overlay { + width: 100px; + + top: 646px; + left: 0; +} + +.pfd-popup-nav-src-overlay .popup-menu-title { + color: var(--wt21-colors-white); + background-color: var(--wt21-colors-blue); +} + +.pfd-popup-brg-src-overlay { + width: 100px; + + top: 646px; + right: 0; +} + +.pfd-popup-brg-src-overlay .popup-menu-title { + color: var(--wt21-colors-white); + background-color: var(--wt21-colors-blue); +} + +.pfd-popup-brg-src-overlay-bottom { + margin-top: 48px; +} + +.pfd-popup-brg-src-arrow { + position: absolute; + overflow: visible; +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsNavBrgSrcMenu.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsNavBrgSrcMenu.tsx new file mode 100644 index 000000000..b1c6f97bf --- /dev/null +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsNavBrgSrcMenu.tsx @@ -0,0 +1,205 @@ +import { ArraySubject, EventBus, FSComponent, Subject, VNode } from '@microsoft/msfs-sdk'; + +import { FloatingRadioList } from '../../Shared/Menus/Components/FloatingRadioList'; +import { FloatingRadioItem } from '../../Shared/Menus/Components/FloatingRadioItem'; +import { PopupSubMenu } from '../../Shared/Menus/Components/PopupSubMenu'; +import { GuiDialog, GuiDialogProps } from '../../Shared/UI/GuiDialog'; +import { WT21BearingPointerControlEvents, WT21CourseNeedleNavIndicator, WT21NavIndicator, WT21NavSource } from '../../Shared/Navigation/WT21NavIndicators'; + +import './PfdSideButtonsNavBrgSrcMenu.css'; + +/** + * Props for {@link PfdSideButtonsNavBrgSrcMenu} + */ +export interface PfdSideButtonsNavBrgSrcMenuProps extends GuiDialogProps { + /** The event bus */ + bus: EventBus, + + /** Used to sync bearing pointer indicator. */ + bearingPointerIndicator1: WT21NavIndicator, + + /** Used to sync bearing pointer indicator. */ + bearingPointerIndicator2: WT21NavIndicator, + + /** Used to sync if isLocalizer. */ + nav1Source: WT21NavSource, + + /** Used to sync if isLocalizer. */ + nav2Source: WT21NavSource, + + /** Used to sync ADF frequency. */ + adfSource: WT21NavSource, + + /** Used to sync active nav source. */ + courseNeedle: WT21CourseNeedleNavIndicator; +} + +/** + * PFD (side button layout) NAV/BRG SRC menu + */ +export class PfdSideButtonsNavBrgSrcMenu extends GuiDialog { + private static readonly BEARING_POINTER_1_PATH = 'M -8 5 L 0 -9 L 8 5 M 0 -16.335 L 0 8'; + + private static readonly BEARING_POINTER_2_PATH = 'M -8.0438 3.7125 L 0 -6.1875 L 8.0438 3.7125 L 0 -6.1875 M -4.3313 -0.6188 L -4.3313 12.1375 M 4.3313 -0.6188 L 4.3313 12.1375'; + + private readonly nav1RadioBox = FSComponent.createRef(); + private readonly nav2RadioBox = FSComponent.createRef(); + private readonly bearingPointer1Option = Subject.create(0); + private readonly bearingPointer2Option = Subject.create(0); + private readonly sources1 = [null, 'FMS1', 'NAV1', 'ADF'] as const; + private readonly sources2 = [null, 'FMS2', 'NAV2', 'ADF'] as const; + private readonly nav1Label = Subject.create(''); + private readonly nav2Label = Subject.create(''); + private readonly adfLabel = Subject.create(''); + + private readonly navSourcesOrder = ['FMS1', 'NAV1', 'NAV2'] as const; + private readonly navSources = ArraySubject.create(['FMS1', 'VOR1', 'VOR2']); + private readonly navSrcOption = Subject.create(0); + /** @inheritdoc */ + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + + this.linkNavIndicators(); + this.linkNavSource(); + } + + // eslint-disable-next-line jsdoc/require-jsdoc + private linkNavIndicators(): void { + this.bearingPointer1Option.sub(x => { + const newSourceName = this.sources1[x]; + this.props.bus.getPublisher() + .pub('nav_ind_bearingPointer1_set_source', newSourceName, true); + }); + this.bearingPointer2Option.sub(x => { + const newSourceName = this.sources2[x]; + this.props.bus.getPublisher() + .pub('nav_ind_bearingPointer2_set_source', newSourceName, true); + }); + + this.props.bearingPointerIndicator1.source.sub(x => { + this.bearingPointer1Option.set(x === null ? 0 : this.sources1.indexOf(x.name as typeof this.sources1[number])); + }, true); + this.props.bearingPointerIndicator2.source.sub(x => { + this.bearingPointer2Option.set(x === null ? 0 : this.sources2.indexOf(x.name as typeof this.sources2[number])); + }, true); + + this.props.nav1Source.isLocalizer.sub(this.updateNav1Label, true); + this.props.nav1Source.isLocalizer.sub(x => this.nav1RadioBox.instance.setIsEnabled(!x), true); + this.props.nav1Source.activeFrequency.sub(this.updateNav1Label, true); + + this.props.nav2Source.isLocalizer.sub(this.updateNav2Label, true); + this.props.nav2Source.isLocalizer.sub(x => this.nav2RadioBox.instance.setIsEnabled(!x), true); + this.props.nav2Source.activeFrequency.sub(this.updateNav2Label, true); + + this.props.adfSource.activeFrequency.sub(this.updateAdfLabel, true); + } + + // eslint-disable-next-line jsdoc/require-jsdoc + private linkNavSource(): void { + this.navSrcOption.sub(x => { + const newSourceName = this.navSourcesOrder[x]; + this.props.courseNeedle.setNewSource(newSourceName); + }); + + this.props.courseNeedle.source.sub(x => { + if (!x) { + return; + } + + this.navSrcOption.set(this.navSourcesOrder.indexOf(x.name as typeof this.navSourcesOrder[number])); + }, true); + + this.props.courseNeedle.isLocalizer.sub(x => { + if (x) { + this.navSources.set(['FMS1', 'LOC1', 'LOC2']); + } else { + this.navSources.set(['FMS1', 'VOR1', 'VOR2']); + } + }, true); + } + + private readonly updateNavLabel = (index: 1 | 2, source: WT21NavSource, label: Subject) => (): void => { + const name = source.isLocalizer.get() ? `LOC${index}` : `VOR${index}`; + const freq = source.activeFrequency.get()?.toFixed(2); + const newLabel = `${name} ${freq}`; + label.set(newLabel); + }; + private readonly updateNav1Label = this.updateNavLabel(1, this.props.nav1Source, this.nav1Label); + private readonly updateNav2Label = this.updateNavLabel(2, this.props.nav2Source, this.nav2Label); + + private readonly updateAdfLabel = (): void => { + const freq = this.props.adfSource.activeFrequency.get()?.toFixed(1); + const newLabel = `ADF~~${freq}`; + this.adfLabel.set(newLabel); + }; + + /** @inheritDoc */ + public onSoftkey1L(): boolean { + this.navSrcOption.set((this.navSrcOption.get() + 1) % 3); + return true; + } + + /** @inheritDoc */ + public onSoftkey1R(): boolean { + this.bearingPointer1Option.set((this.bearingPointer1Option.get() + 1) % 3); + return true; + } + + /** @inheritDoc */ + public onSoftkey3R(): boolean { + this.bearingPointer2Option.set((this.bearingPointer2Option.get() + 1) % 2); + return true; + } + + /** @inheritDoc */ + public override render(): VNode { + return ( +
+ + + FMS + VOR1 + VOR2 + + + + + + + + + + + + + + OFF + FMS + VOR1 + ADF1 + + + + OFF + FMS + VOR2 + + +
+ ); + } +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs1Menu.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs1Menu.tsx new file mode 100644 index 000000000..fd7484aa1 --- /dev/null +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs1Menu.tsx @@ -0,0 +1,265 @@ +import { + ClockEvents, EventBus, FlightPlanner, FocusPosition, FSComponent, MinimumsEvents, MinimumsMode, MutableSubscribable, NodeReference, Subject, VNode, +} from '@microsoft/msfs-sdk'; + +import { PopupSubMenu } from '../../Shared/Menus/Components/PopupSubMenu'; +import { GuiDialog, GuiDialogProps } from '../../Shared/UI/GuiDialog'; +import { VSpeedUserSettings } from '../../Shared/Profiles/VSpeedUserSettings'; +import { VSpeedType } from '../../Shared/ReferenceSpeeds'; +import { CheckBoxNumeric, CheckBoxNumericStyle } from '../../Shared/Menus/Components/CheckboxNumeric'; +import { RefsUserSettings } from '../../Shared/Profiles/RefsUserSettings'; +import { GuiHEvent } from '../../Shared/UI/GuiHEvent'; + +import './PfdSideButtonsRefsMenu.css'; + +/** + * Props for {@link PfdSideButtonsRefs1Menu} + */ +export interface PfdSideButtonsRefs1MenuProps extends GuiDialogProps { + /** The event bus */ + bus: EventBus, + + /** An instance of the flight planner. */ + planner: FlightPlanner; +} + +/** + * PFD (side button layout) REFS menu + */ +export class PfdSideButtonsRefs1Menu extends GuiDialog { + private readonly vSpeedSettings = new VSpeedUserSettings(this.props.bus); + + private readonly VSpeedUiRefs = new Map>([ + [VSpeedType.V1, new NodeReference()], + [VSpeedType.V2, new NodeReference()], + [VSpeedType.Vr, new NodeReference()], + [VSpeedType.Venr, new NodeReference()], + [VSpeedType.Vapp, new NodeReference()], + [VSpeedType.Vref, new NodeReference()], + ]); + + private readonly raMinRef = FSComponent.createRef(); + + private readonly baroMinRef = FSComponent.createRef(); + + private readonly VSpeedValues: Record> = { + [VSpeedType.V1]: this.vSpeedSettings.getSettings(VSpeedType.V1).value, + [VSpeedType.V2]: this.vSpeedSettings.getSettings(VSpeedType.V2).value, + [VSpeedType.Vr]: this.vSpeedSettings.getSettings(VSpeedType.Vr).value, + [VSpeedType.Venr]: this.vSpeedSettings.getSettings(VSpeedType.Venr).value, + [VSpeedType.Vapp]: this.vSpeedSettings.getSettings(VSpeedType.Vapp).value, + [VSpeedType.Vref]: this.vSpeedSettings.getSettings(VSpeedType.Vref).value, + }; + + private readonly VSpeedStates: Record> = { + [VSpeedType.V1]: this.vSpeedSettings.getSettings(VSpeedType.V1).show, + [VSpeedType.V2]: this.vSpeedSettings.getSettings(VSpeedType.V2).show, + [VSpeedType.Vr]: this.vSpeedSettings.getSettings(VSpeedType.Vr).show, + [VSpeedType.Venr]: this.vSpeedSettings.getSettings(VSpeedType.Venr).show, + [VSpeedType.Vapp]: this.vSpeedSettings.getSettings(VSpeedType.Vapp).show, + [VSpeedType.Vref]: this.vSpeedSettings.getSettings(VSpeedType.Vref).show, + }; + + private readonly VSpeedCssClasses = new Map>([ + [VSpeedType.V1, Subject.create('')], + [VSpeedType.Vr, Subject.create('')], + [VSpeedType.V2, Subject.create('')], + [VSpeedType.Venr, Subject.create('')], + [VSpeedType.Vapp, Subject.create('')], + [VSpeedType.Vref, Subject.create('')] + ]); + + private readonly rs = RefsUserSettings.getManager(this.props.bus); + + private readonly minimumsSelectedIndex = Subject.create(MinimumsMode.OFF); + private readonly baroMinimums = Subject.create(-1); + private readonly raMinimums = Subject.create(-1); + private _now = 0; + private debounceTimer = 0; + private isBusy = false; + + /** @inheritdoc */ + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + + for (const [type, uiRef] of this.VSpeedUiRefs.entries()) { + const settings = this.vSpeedSettings.getSettings(type); + uiRef.instance.props.onValueChanged = () => { + settings.manual.set(true); + }; + settings.manual.sub((v) => { + this.VSpeedCssClasses.get(type)?.set((v as boolean) ? '' : 'magenta'); + }, true); + } + + this.props.bus.getSubscriber().on('realTime').whenChangedBy(100).handle(this.onClock.bind(this)); + + const mins = this.props.bus.getSubscriber(); + mins.on('minimums_mode').handle((v) => { + if (this.isBusy) { return; } + this.minimumsSelectedIndex.set(v); + this.rs.getSetting('minsmode').value = v; + }); + mins.on('decision_altitude_feet').handle((v) => { + if (this.isBusy) { return; } + this.baroMinimums.set(v); + if (v > 0) { + this.rs.getSetting('baromins').value = v; + } + }); + mins.on('decision_height_feet').handle((v) => { + if (this.isBusy) { return; } + this.raMinimums.set(v); + if (v > 0) { + this.rs.getSetting('radiomins').value = v; + } + }); + + const minsPub = this.props.bus.getPublisher(); + this.minimumsSelectedIndex.sub(v => { + this.isBusy = true; + this.debounceTimer = this._now; + minsPub.pub('set_minimums_mode', v, false, true); + }); + this.baroMinimums.sub(v => { + this.isBusy = true; + this.debounceTimer = this._now; + minsPub.pub('set_decision_altitude_feet', v, true, true); + }); + this.raMinimums.sub(v => { + this.isBusy = true; + this.debounceTimer = this._now; + minsPub.pub('set_decision_height_feet', v, true, true); + }); + + // set saved setting values on load + this.minimumsSelectedIndex.set(this.rs.getSetting('minsmode').value); + this.baroMinimums.set(this.rs.getSetting('baromins').value); + this.raMinimums.set(this.rs.getSetting('radiomins').value); + } + + /** @inheritDoc */ + public onInteractionEvent(evt: GuiHEvent): boolean { + switch (evt) { + case GuiHEvent.SOFTKEY_1L: return this.VSpeedUiRefs.get(VSpeedType.Venr)?.instance.focus(FocusPosition.None) ?? false; + case GuiHEvent.SOFTKEY_2L: return this.VSpeedUiRefs.get(VSpeedType.V2)?.instance.focus(FocusPosition.None) ?? false; + case GuiHEvent.SOFTKEY_3L: return this.VSpeedUiRefs.get(VSpeedType.Vr)?.instance.focus(FocusPosition.None) ?? false; + case GuiHEvent.SOFTKEY_4L: return this.VSpeedUiRefs.get(VSpeedType.V1)?.instance.focus(FocusPosition.None) ?? false; + case GuiHEvent.SOFTKEY_1R: return this.raMinRef.instance.focus(FocusPosition.None); + case GuiHEvent.SOFTKEY_2R: return this.baroMinRef.instance.focus(FocusPosition.None); + case GuiHEvent.SOFTKEY_3R: return this.VSpeedUiRefs.get(VSpeedType.Vapp)?.instance.focus(FocusPosition.None) ?? false; + case GuiHEvent.SOFTKEY_4R: return this.VSpeedUiRefs.get(VSpeedType.Vref)?.instance.focus(FocusPosition.None) ?? false; + } + + return super.onInteractionEvent(evt); + } + + /** + * Updates the time; resets the isBusy flag. + * @param time The real time. + */ + private onClock(time: number): void { + this._now = time; + if (this._now - this.debounceTimer > 500) { + this.isBusy = false; + } + } + + /** @inheritdoc */ + public render(): VNode { + return ( +
+ + + + + + + + + index === MinimumsMode.RA)} + onCheckedChanged={(newValue) => this.minimumsSelectedIndex.set(newValue ? MinimumsMode.RA : MinimumsMode.OFF)} + dataRef={this.raMinimums} + max={2500} + style={CheckBoxNumericStyle.SideButtonBound} + orientation="right" + /> + + index === MinimumsMode.BARO)} + onCheckedChanged={(newValue) => this.minimumsSelectedIndex.set(newValue ? MinimumsMode.BARO : MinimumsMode.OFF)} + dataRef={this.baroMinimums} + increments={10} + style={CheckBoxNumericStyle.SideButtonBound} + orientation="right" + /> + + + + + +
+ ); + } +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs2Menu.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs2Menu.css new file mode 100644 index 000000000..c4abc11f7 --- /dev/null +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs2Menu.css @@ -0,0 +1,11 @@ +.pfd-popup-refs-2 { + height: 350px; +} + +.pfd-popup-refs-1 .popup-menu-radio[data-label="PRESSURE"] { + margin-top: 6px; +} + +.pfd-popup-refs-1 .popup-menu-radio:not([data-label="PRESSURE"]) { + margin-top: 36px; +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs2Menu.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs2Menu.tsx new file mode 100644 index 000000000..c61e89a61 --- /dev/null +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefs2Menu.tsx @@ -0,0 +1,137 @@ +import { EventBus, FlightPlanner, FocusPosition, FSComponent, Subject, VNode } from '@microsoft/msfs-sdk'; + +import { PopupSubMenu } from '../../Shared/Menus/Components/PopupSubMenu'; +import { GuiDialog, GuiDialogProps } from '../../Shared/UI/GuiDialog'; +import { GuiHEvent } from '../../Shared/UI/GuiHEvent'; +import { RadioList, RadioListStyle } from '../../Shared/Menus/Components/RadioList'; +import { RadioBox } from '../../Shared/Menus/Components/Radiobox'; +import { PFDUserSettings } from '../PFDUserSettings'; + +import './PfdSideButtonsRefs2Menu.css'; + +/** + * Props for {@link PfdSideButtonsRefsMenu} + */ +export interface PfdSideButtonsRefs2MenuProps extends GuiDialogProps { + /** The event bus */ + bus: EventBus, + + /** An instance of the flight planner. */ + planner: FlightPlanner; +} + +/** + * PFD (side button layout) REFS menu + */ +export class PfdSideButtonsRefs2Menu extends GuiDialog { + private readonly elementRefs = [ + FSComponent.createRef(), + FSComponent.createRef(), + FSComponent.createRef(), + FSComponent.createRef(), + ]; + + private readonly pressureOption = Subject.create(0); + private readonly fltDirOption = Subject.create(0); + private readonly metricOption = Subject.create(0); + private readonly flAlertOption = Subject.create(0); + + private readonly pfdSettingsManager = PFDUserSettings.getManager(this.props.bus); + + /** @inheritDoc */ + public onAfterRender(node: VNode): void { + super.onAfterRender(node); + + this.linkPressureOption(); + this.linkMetricSetting(); + this.linkFlAlertSetting(); + this.linkFDStyle(); + } + + /** @inheritDoc */ + public onInteractionEvent(evt: GuiHEvent): boolean { + switch (evt) { + case GuiHEvent.SOFTKEY_1L: return this.elementRefs[0].instance.focus(FocusPosition.None) ?? false; + case GuiHEvent.SOFTKEY_2L: return this.elementRefs[1].instance.focus(FocusPosition.None) ?? false; + case GuiHEvent.SOFTKEY_3L: return this.elementRefs[2].instance.focus(FocusPosition.None) ?? false; + case GuiHEvent.SOFTKEY_4L: return this.elementRefs[3].instance.focus(FocusPosition.None) ?? false; + } + + return super.onInteractionEvent(evt); + } + + // eslint-disable-next-line jsdoc/require-jsdoc + private linkPressureOption(): void { + const fltDirStyle = this.pfdSettingsManager.getSetting('pressureUnitHPA'); + this.pressureOption.sub(x => { + fltDirStyle.value = !!x; + }); + this.pfdSettingsManager.whenSettingChanged('pressureUnitHPA').handle(x => { + this.pressureOption.set(x ? 1 : 0); + }); + } + + // eslint-disable-next-line jsdoc/require-jsdoc + private linkMetricSetting(): void { + const metricSetting = this.pfdSettingsManager.getSetting('altMetric'); + this.metricOption.sub(x => { + metricSetting.value = !!x; + }); + this.pfdSettingsManager.whenSettingChanged('altMetric').handle(x => { + this.metricOption.set(x ? 1 : 0); + }); + } + + // eslint-disable-next-line jsdoc/require-jsdoc + private linkFlAlertSetting(): void { + const flAlertSetting = this.pfdSettingsManager.getSetting('flightLevelAlert'); + this.flAlertOption.sub(x => { + flAlertSetting.value = !!x; + }); + this.pfdSettingsManager.whenSettingChanged('flightLevelAlert').handle(x => { + this.flAlertOption.set(x ? 1 : 0); + }); + } + + // eslint-disable-next-line jsdoc/require-jsdoc + private linkFDStyle(): void { + const fltDirStyle = this.pfdSettingsManager.getSetting('fltDirStyle'); + this.fltDirOption.sub(x => { + fltDirStyle.value = !!x; + }); + this.pfdSettingsManager.whenSettingChanged('fltDirStyle').handle(x => { + this.fltDirOption.set(x ? 1 : 0); + }); + } + + /** @inheritdoc */ + public render(): VNode { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ ); + } +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefsMenu.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefsMenu.css new file mode 100644 index 000000000..f01cce174 --- /dev/null +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/Menus/PfdSideButtonsRefsMenu.css @@ -0,0 +1,35 @@ +.pfd-popup-refs-1 { + top: 646px; + left: 0; +} + +.pfd-popup-refs-1 .popup-menu-title { + width: 112px; + + color: var(--wt21-colors-white); + background-color: var(--wt21-colors-blue); +} + +.pfd-popup-refs-2 { + top: 646px; + right: 0; +} + +.pfd-popup-refs-2 .popup-menu-title { + width: 112px; + + margin-left: auto; + + color: var(--wt21-colors-white); + background-color: var(--wt21-colors-blue); +} + +.pfd-popup-refs-1 .popup-menu-checkbox-side-button[data-label="VT"], +.pfd-popup-refs-2 .popup-menu-checkbox-side-button[data-label="RA MIN"] { + margin-top: 6px; +} + +.pfd-popup-refs-1 .popup-menu-checkbox-side-button:not([data-label="VT"]), +.pfd-popup-refs-2 .popup-menu-checkbox-side-button:not([data-label="RA MIN"]) { + margin-top: 36px; +} \ No newline at end of file diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/WT21_PFD_Instrument.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/WT21_PFD_Instrument.tsx index fa779aa28..ba463ea45 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/WT21_PFD_Instrument.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/PFD/WT21_PFD_Instrument.tsx @@ -6,13 +6,10 @@ /// import { - AdcPublisher, AhrsPublisher, AutopilotInstrument, BaseInstrumentPublisher, BasicAvionicsSystem, Clock, ControlPublisher, - ControlSurfacesPublisher, DefaultUserSettingManager, ElectricalPublisher, EventBus, FacilityLoader, FacilityRepository, - FlightPathAirplaneSpeedMode, - FlightPathCalculator, FlightPlanner, FSComponent, FsInstrument, GNSSPublisher, HEventPublisher, InstrumentBackplane, LNavSimVarPublisher, - MinimumsEvents, MinimumsManager, MinimumsSimVarPublisher, NavComSimVarPublisher, - SimVarValueType, TrafficInstrument, UserSettingSaveManager, VNavSimVarPublisher, Wait, - XPDRSimVarPublisher + AdcPublisher, AhrsPublisher, AutopilotInstrument, BaseInstrumentPublisher, BasicAvionicsSystem, Clock, ControlPublisher, ControlSurfacesPublisher, DefaultUserSettingManager, + ElectricalPublisher, EventBus, FacilityLoader, FacilityRepository, FlightPathAirplaneSpeedMode, FlightPathCalculator, FlightPlanner, FSComponent, GNSSPublisher, HEventPublisher, + InstrumentBackplane, LNavSimVarPublisher, MinimumsEvents, MinimumsManager, MinimumsSimVarPublisher, NavComSimVarPublisher, SimVarValueType, TrafficInstrument, + UserSettingSaveManager, VNavSimVarPublisher, Wait, XPDRSimVarPublisher, } from '@microsoft/msfs-sdk'; import { WT21LNavDataSimVarPublisher } from '../FMC/Autopilot/WT21LNavDataEvents'; @@ -24,8 +21,8 @@ import { LowerSectionContainer } from '../Shared/LowerSection/LowerSectionContai import { MenuContainer } from '../Shared/Menus/MenuContainer'; import { AdfRadioSource, GpsSource, initNavIndicatorContext, NavIndicatorContext, NavIndicators, NavRadioNavSource, NavSources } from '../Shared/Navigation'; import { - WT21BearingPointerNavIndicator, WT21CourseNeedleNavIndicator, WT21GhostNeedleNavIndicator, WT21NavIndicator, WT21NavIndicatorName, WT21NavIndicators, - WT21NavSourceNames, WT21NavSources + WT21BearingPointerNavIndicator, WT21CourseNeedleNavIndicator, WT21GhostNeedleNavIndicator, WT21NavIndicator, WT21NavIndicatorName, WT21NavIndicators, WT21NavSourceNames, + WT21NavSources, } from '../Shared/Navigation/WT21NavIndicators'; import { PerformancePlanRepository } from '../Shared/Performance/PerformancePlanRepository'; import { WT21ControlPublisher } from '../Shared/WT21ControlEvents'; @@ -49,17 +46,21 @@ import { PfdRefsMenu } from './Menus/PfdRefsMenu'; import { AOASystemEvents, WT21ElectricalSetup } from '../Shared/Systems'; import { Gpws } from '../Shared/Systems/gpws/Gpws'; import { CJ4CabinLightsSystem } from '../Shared/Systems/CJ4CabinLightsSystem'; - -import '../Shared/WT21_Common.css'; -import './WT21_PFD.css'; import { WT21FixInfoManager } from '../FMC/Systems/WT21FixInfoManager'; import { WT21Fms } from '../Shared/FlightPlan/WT21Fms'; import { WT21FixInfoConfig } from '../FMC/Systems/WT21FixInfoConfig'; +import { WT21DisplayUnitFsInstrument, WT21DisplayUnitType } from '../Shared/WT21DisplayUnitFsInstrument'; +import { PfdSideButtonsNavBrgSrcMenu } from './Menus/PfdSideButtonsNavBrgSrcMenu'; +import { PfdSideButtonsRefs1Menu } from './Menus/PfdSideButtonsRefs1Menu'; +import { PfdSideButtonsRefs2Menu } from './Menus/PfdSideButtonsRefs2Menu'; + +import '../Shared/WT21_Common.css'; +import './WT21_PFD.css'; /** * The WT21 PFD Instrument */ -export class WT21_PFD_Instrument implements FsInstrument { +export class WT21_PFD_Instrument extends WT21DisplayUnitFsInstrument { private readonly bus: EventBus; private readonly baseInstrumentPublisher: BaseInstrumentPublisher; private readonly hEventPublisher: HEventPublisher; @@ -109,6 +110,8 @@ export class WT21_PFD_Instrument implements FsInstrument { * @param instrument The base instrument. */ constructor(readonly instrument: BaseInstrument) { + super(instrument, WT21DisplayUnitType.Pfd, 1); // TODO if we add support for multiple PFDs, adapt this + SimVar.SetSimVarValue('L:WT21_BETA_VERSION', 'number', 15); RegisterViewListener('JS_LISTENER_INSTRUMENTS'); @@ -215,8 +218,10 @@ export class WT21_PFD_Instrument implements FsInstrument { ); this.backplane.addInstrument('navSources', this.navSources); + const courseNeedleNavIndicator = new WT21CourseNeedleNavIndicator(this.navSources, this, this.bus); + this.navIndicators = new NavIndicators(new Map([ - ['courseNeedle', new WT21CourseNeedleNavIndicator(this.navSources, this.bus, 'PFD')], + ['courseNeedle', courseNeedleNavIndicator], ['ghostNeedle', new WT21GhostNeedleNavIndicator(this.navSources, this.bus)], ['bearingPointer1', new WT21BearingPointerNavIndicator(this.navSources, this.bus, 1, 'NAV1')], ['bearingPointer2', new WT21BearingPointerNavIndicator(this.navSources, this.bus, 2, 'NAV2')], @@ -232,7 +237,7 @@ export class WT21_PFD_Instrument implements FsInstrument { this.minimumsAlertController = new MinimumsAlertController(this.bus); - this.pfdMenuViewService = new PfdMenuViewService(); + this.pfdMenuViewService = new PfdMenuViewService(this.bus, this.displayUnitConfig, courseNeedleNavIndicator); // FIXME Add route predictor when FlightPlanPredictor refactored to implement FlightPlanPredictionsProvider this.fixInfoManager = new WT21FixInfoManager(this.bus, this.facLoader, WT21Fms.PRIMARY_ACT_PLAN_INDEX, this.planner, WT21FixInfoConfig /*, this.activeRoutePredictor*/); @@ -269,6 +274,7 @@ export class WT21_PFD_Instrument implements FsInstrument { ); this.pfdMenuViewService.registerView('PfdOverlaysMenu', () => ); this.pfdMenuViewService.registerView('PfdBaroSetMenu', () => ); + this.pfdMenuViewService.registerView( + 'PfdSideButtonsNavBrgSrcMenu', + () => + + ); + this.pfdMenuViewService.registerView( + 'PfdSideButtonsRefs1Menu', + () => + + ); + this.pfdMenuViewService.registerView( + 'PfdSideButtonsRefs2Menu', + () => + + ); SimVar.SetSimVarValue('L:AS3000_Brightness', 'number', 0.85); this.clock.init(); diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Config/DisplayUnitConfig.ts b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Config/DisplayUnitConfig.ts new file mode 100644 index 000000000..06e5189b6 --- /dev/null +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Config/DisplayUnitConfig.ts @@ -0,0 +1,56 @@ +export enum DisplayUnitLayout { + /** Traditional layout, without softkeys */ + Traditional = 'Traditional', + + /** Layout with 4 softkeys on each side */ + Softkeys = 'Softkeys', +} + +/** + * WT21 Display Unit configuration + */ +export interface DisplayUnitConfigInterface { + /** The layout of the display unit */ + displayUnitLayout: DisplayUnitLayout; +} + +/** + * WT21 Display Unit configuration + */ +export class DisplayUnitConfig implements DisplayUnitConfigInterface { + public static readonly DEFAULT: DisplayUnitConfigInterface = { + displayUnitLayout: DisplayUnitLayout.Traditional, + }; + + public displayUnitLayout = DisplayUnitLayout.Traditional; + + /** + * Constructs a DisplayUnitConfig from an element + * @param element the XML element + */ + constructor(element: Element) { + if (element.tagName != 'DisplayUnitConfig') { + throw new Error(`Invalid DisplayUnitConfig definition: expected tag name 'DisplayUnitConfig' but was '${element.tagName}'`); + } + + const displayUnitLayoutTags = element.querySelectorAll(':scope > Layout'); + + if (displayUnitLayoutTags.length > 0) { + if (displayUnitLayoutTags.length > 1) { + console.warn('Invalid DisplayUnitConfig definition: Multiple \'Layout\' tags inside \'DisplayUnitConfig\'. Only the first one will be taken into account'); + } + + const layout = displayUnitLayoutTags.item(0).textContent; + + if (layout === null) { + throw new Error('Invalid Layout definition: content is mandatory'); + } + + if (!(layout in DisplayUnitLayout)) { + throw new Error('Invalid Layout definition: content must be a valid DisplayUnitLayout value'); + } + + this.displayUnitLayout = (DisplayUnitLayout as Record)[layout]; + } + } +} \ No newline at end of file diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlan/WT21Fms.ts b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlan/WT21Fms.ts index d4633892d..aaa550359 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlan/WT21Fms.ts +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlan/WT21Fms.ts @@ -1461,38 +1461,116 @@ export class WT21Fms { this.setApproachDetails(false, ApproachType.APPROACH_TYPE_UNKNOWN, RnavTypeFlags.None, false, false, null); plan.calculate(0); } - /** - * Checks whether the procedure being modified contains the currently active from and to legs and, if so, - * returns those two legs. If the active leg is a direct to, this returns the entire direct to sequence (3 legs) - * @param plan The flight plan. - * @param segmentIndex The Segment Index. - * @returns The array of active legs. + * Gets an array of legs in a flight plan segment ending with the active leg that are to be displaced if the segment + * is deleted. + * @param plan The flight plan containing the segment for which to get the array. + * @param segmentIndex The index of the segment for which to get the array. + * @returns An array of legs in the specified flight plan segment ending with the active leg that are to be + * displaced if the segment is deleted, or `undefined` if the segment does not contain the active leg. */ - private getActiveLegsInCurrentProcedure(plan: FlightPlan, segmentIndex: number): FlightPlanLeg[] | undefined { + private getSegmentActiveLegsToDisplace(plan: FlightPlan, segmentIndex: number): LegDefinition[] | undefined { + const segment = plan.getSegment(segmentIndex); + const activeLeg = segment.legs[plan.activeLateralLeg - segment.offset] as LegDefinition | undefined; - if (plan.getSegmentIndex(plan.activeLateralLeg) === segmentIndex) { + // If active leg is not in the segment to be deleted, then nothing needs to be displaced + if (!activeLeg) { + return undefined; + } - const currentToLeg = plan.tryGetLeg(plan.activeLateralLeg); - const currentFromLeg = plan.tryGetLeg(plan.activeLateralLeg - 1); + if (BitFlags.isAll(activeLeg.flags, LegDefinitionFlags.DirectTo)) { + // Active leg is part of a direct-to. - if (!currentToLeg || !currentFromLeg) { + if (plan.directToData.segmentIndex !== segmentIndex || plan.directToData.segmentLegIndex + WT21FmsUtils.DTO_LEG_OFFSET >= segment.legs.length) { + // If the plan's direct to data does not match what we would expect given the active leg, then something has + // gone wrong with the flight plan's state and we will just bail. return undefined; } - const newToLeg = Object.assign({}, currentToLeg.leg); - const newFromLeg = Object.assign({}, currentFromLeg.leg); + return segment.legs.slice(0, plan.directToData.segmentLegIndex + WT21FmsUtils.DTO_LEG_OFFSET + 1); + } else { + // Active leg is not part of a direct-to. In this case we return every leg in the active segment prior to and + // including the active leg. - if (BitFlags.isAll(currentToLeg.flags, WT21LegDefinitionFlags.DirectTo)) { - const discoLeg = Object.assign({}, plan.getLeg(plan.activeLateralLeg - 2).leg); - return [discoLeg, newFromLeg, newToLeg]; - } else { - return [newFromLeg, newToLeg]; + return segment.legs.slice(0, plan.activeLateralLeg - segment.offset + 1); + } + } + + /** + * Displaces a sequence of flight plan legs contained in a now-removed segment ending with the active leg into a new + * flight plan segment. If the displaced active leg was a direct-to leg, then a new direct-to will be created to the + * displaced target leg. Otherwise, the active leg is set to the displaced active leg. + * @param plan The flight plan into which to displace the legs. + * @param segmentIndex The index of the flight plan segment into which to displace the legs. + * @param activeLegArray The sequence of flight plan legs to displace. The sequence should contain all of the legs + * contained in the former active segment up to and including the active leg. + * @param insertAtEnd Whether to insert the displaced legs at the end of the segment instead of the beginning. + */ + private displaceActiveLegsIntoSegment(plan: FlightPlan, segmentIndex: number, activeLegArray: LegDefinition[], insertAtEnd: boolean): void { + WT21FmsUtils.removeDisplacedActiveLegs(plan); + + const segment = plan.getSegment(segmentIndex); + + const insertAtIndex = insertAtEnd ? segment.legs.length : 0; + + if (insertAtEnd) { + plan.addLeg(segmentIndex, FlightPlan.createLeg({ type: LegType.Discontinuity }), insertAtIndex, WT21LegDefinitionFlags.DisplacedActiveLeg); + } else { + const segmentFirstLeg = segment.legs[0]; + + // We don't want to insert duplicate discontinuities if there is already one at the start of the segment + const discontinuityAlreadyPresent = segmentFirstLeg && WT21FmsUtils.isDiscontinuityLeg(segmentFirstLeg.leg.type); + + if (!discontinuityAlreadyPresent) { + plan.addLeg(segmentIndex, FlightPlan.createLeg({ type: LegType.Discontinuity }), insertAtIndex, WT21LegDefinitionFlags.DisplacedActiveLeg); } + } + + // The active leg is guaranteed to be the last leg in the array. + const activeLeg = activeLegArray[activeLegArray.length - 1]; + const isActiveLegDto = BitFlags.isAll(activeLeg.flags, LegDefinitionFlags.DirectTo); + const dtoTargetLegIndex = isActiveLegDto ? activeLegArray.length - 1 - WT21FmsUtils.DTO_LEG_OFFSET : undefined; + const dtoTargetLeg = dtoTargetLegIndex !== undefined ? activeLegArray[dtoTargetLegIndex] : undefined; + let displacedDtoTargetLegIndex = undefined; + // Add all displaced legs to the segment, skipping any active direct-to legs. + for (let i = dtoTargetLegIndex ?? activeLegArray.length - 1; i >= 0; i--) { + const leg = activeLegArray[i]; + + const newLeg = FlightPlan.createLeg(leg.leg); + + // Displaced legs aren't a part of a procedure anymore, so we clear the fix type flags + newLeg.fixTypeFlags = 0; + + plan.addLeg(segmentIndex, newLeg, insertAtIndex, WT21LegDefinitionFlags.DisplacedActiveLeg); + + // Preserve altitude and speed restrictions on the active leg or direct-to target leg only. + if (leg === activeLeg || leg === dtoTargetLeg) { + plan.setLegVerticalData(segmentIndex, insertAtIndex, leg.verticalData); + } + + // Check if we are displacing an inactive direct-to leg. If so, mark the index of the corresponding displaced + // direct-to target leg so we can deal with it below. + if (displacedDtoTargetLegIndex === undefined && BitFlags.isAll(leg.flags, LegDefinitionFlags.DirectTo)) { + displacedDtoTargetLegIndex = i - WT21FmsUtils.DTO_LEG_OFFSET + insertAtIndex; + } + } + + // If we displaced an inactive direct-to sequence, then we need to update the plan's direct-to data to match the + // indexes of the now displaced direct-to target leg. + if (displacedDtoTargetLegIndex !== undefined) { + plan.setDirectToData(segmentIndex, displacedDtoTargetLegIndex); + } + + if (dtoTargetLegIndex !== undefined) { + const course = activeLeg.leg.type === LegType.CF ? activeLeg.leg.course : undefined; + this.createDirectTo(segmentIndex, dtoTargetLegIndex + insertAtIndex, undefined, course, undefined); + } else { + const newActiveLegIndex = segment.offset + insertAtIndex + activeLegArray.length - 1; + plan.setCalculatingLeg(newActiveLegIndex); + plan.setLateralLeg(newActiveLegIndex); } - return undefined; } /** @@ -1516,7 +1594,7 @@ export class WT21Fms { // Grabbing the active legs (if there are any) in the existing departure semgent, // so that we can put them somewhere after clearing the segment. - const activeLegArray = !Simplane.getIsGrounded() && plan.activeLateralLeg > 0 ? this.getActiveLegsInCurrentProcedure(plan, segmentIndex) : undefined; + const activeLegArray = !Simplane.getIsGrounded() && plan.activeLateralLeg > 0 ? this.getSegmentActiveLegsToDisplace(plan, segmentIndex) : undefined; this.planClearSegment(segmentIndex, FlightPlanSegmentType.Departure); @@ -1536,43 +1614,12 @@ export class WT21Fms { this.planRemoveDuplicateLeg(lastDepLeg, nextLeg); } - if (activeLegArray) { - WT21FmsUtils.removeDisplacedActiveLegs(plan); - - const segmentFirstLeg = plan.getSegment(segmentIndex).legs[0]; - - // We don't want to insert duplicate discontinuities if there is already one at the start of the approach - const discontinuityAlreadyPresent = segmentFirstLeg && WT21FmsUtils.isDiscontinuityLeg(segmentFirstLeg.leg.type); - - if (activeLegArray.length === 2) { - if (!discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, FlightPlan.createLeg({ type: LegType.Discontinuity }), 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - if (!WT21FmsUtils.isDiscontinuityLeg(activeLegArray[1].type) || !discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, activeLegArray[1], 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } + this.generateSegmentVerticalData(plan, segmentIndex); - if (!WT21FmsUtils.isDiscontinuityLeg(activeLegArray[0].type) || !discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, activeLegArray[0], 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - plan.setLateralLeg(depSegment.offset + 1); - } else if (activeLegArray.length === 3) { - if (!discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, FlightPlan.createLeg({ type: LegType.Discontinuity }), 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - if (!WT21FmsUtils.isDiscontinuityLeg(activeLegArray[2].type) || !discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, activeLegArray[2], 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - this.createDirectTo(segmentIndex, 0); - } + if (activeLegArray) { + this.displaceActiveLegsIntoSegment(plan, segmentIndex, activeLegArray, false); } - this.setVerticalData(plan, segmentIndex); - plan.calculate(0); } @@ -1679,7 +1726,7 @@ export class WT21Fms { const segmentIndex = this.ensureOnlyOneSegmentOfType(FlightPlanSegmentType.Arrival); - const activeLegArray = this.getActiveLegsInCurrentProcedure(plan, segmentIndex); + const activeLegArray = this.getSegmentActiveLegsToDisplace(plan, segmentIndex); let arrivalActiveLegIcao: undefined | string; @@ -1716,8 +1763,11 @@ export class WT21Fms { const arrSegment = plan.getSegment(segmentIndex); const prevLeg = plan.getPrevLeg(segmentIndex, 0); const firstArrLeg = arrSegment.legs[0]; + + let deduplicatedEnrouteLeg: LegDefinition | null = null; + if (prevLeg && firstArrLeg && this.isDuplicateLeg(prevLeg.leg, firstArrLeg.leg)) { - this.planRemoveDuplicateLeg(prevLeg, firstArrLeg); + deduplicatedEnrouteLeg = this.planRemoveDuplicateLeg(prevLeg, firstArrLeg); } const nextLeg = plan.getNextLeg(segmentIndex, Infinity); @@ -1731,41 +1781,21 @@ export class WT21Fms { this.activateLeg(segmentIndex, arrSegment.legs.length - 1); } - this.tryInsertDiscontinuity(plan, segmentIndex); + // If we didn't remove a duplicate, insert a discontinuity at the start of the arrival + if (!deduplicatedEnrouteLeg && (!prevLeg || !WT21FmsUtils.isVectorsLeg(prevLeg.leg.type))) { + this.tryInsertDiscontinuity(plan, segmentIndex); + } + + this.generateSegmentVerticalData(plan, segmentIndex); const matchingActiveProcedureLegIndex = WT21FmsUtils.findIcaoInSegment(arrSegment, arrivalActiveLegIcao); if (activeLegArray && matchingActiveProcedureLegIndex === undefined) { - WT21FmsUtils.removeDisplacedActiveLegs(plan); - - const segmentFirstLeg = plan.getSegment(segmentIndex).legs[0]; - - // We don't want to insert duplicate discontinuities if there is already one at the start of the approach - const discontinuityAlreadyPresent = segmentFirstLeg && WT21FmsUtils.isDiscontinuityLeg(segmentFirstLeg.leg.type); - - if (activeLegArray.length === 2) { - if (!WT21FmsUtils.isDiscontinuityLeg(activeLegArray[1].type) || !discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, activeLegArray[1], 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - if (!WT21FmsUtils.isDiscontinuityLeg(activeLegArray[0].type) || !discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, activeLegArray[0], 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - plan.setLateralLeg(arrSegment.offset + 1); - } else if (activeLegArray.length === 3) { - if (!WT21FmsUtils.isDiscontinuityLeg(activeLegArray[2].type) || !discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, activeLegArray[2], 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - this.createDirectTo(segmentIndex, 0); - } + this.displaceActiveLegsIntoSegment(plan, segmentIndex, activeLegArray, false); } else if (matchingActiveProcedureLegIndex !== undefined) { plan.setLateralLeg(arrSegment.offset + matchingActiveProcedureLegIndex); } - this.setVerticalData(plan, segmentIndex); - this.cleanupLegsAfterApproach(plan); this.tryConnectProcedures(plan); @@ -2073,7 +2103,7 @@ export class WT21Fms { const segmentIndex = this.ensureOnlyOneSegmentOfType(FlightPlanSegmentType.Approach); - const activeLegArray = this.getActiveLegsInCurrentProcedure(plan, segmentIndex); + const activeLegArray = this.getSegmentActiveLegsToDisplace(plan, segmentIndex); const apprSegment = plan.getSegment(segmentIndex); @@ -2087,6 +2117,8 @@ export class WT21Fms { if (insertProcedureObject.runway) { plan.setDestinationRunway(insertProcedureObject.runway); + } else { + plan.setDestinationRunway(undefined); } let haveAddedMap = false; @@ -2113,8 +2145,11 @@ export class WT21Fms { const prevLeg = plan.getPrevLeg(segmentIndex, 0); const firstAppLeg = apprSegment.legs[0]; + + let deduplicatedArrivalLeg: LegDefinition | null = null; + if (prevLeg && firstAppLeg && this.isDuplicateLeg(prevLeg.leg, firstAppLeg.leg)) { - this.planRemoveDuplicateLeg(prevLeg, firstAppLeg); + deduplicatedArrivalLeg = this.planRemoveDuplicateLeg(prevLeg, firstAppLeg); } // Adds missed approach legs @@ -2166,43 +2201,21 @@ export class WT21Fms { if (activeLegArray) { WT21FmsUtils.removeDisplacedActiveLegs(plan); - WT21FmsUtils.removeFixTypeFlags(activeLegArray); - - const segmentFirstLeg = plan.getSegment(segmentIndex).legs[0]; - - // We don't want to insert duplicate discontinuities if there is already one at the start of the approach - const discontinuityAlreadyPresent = segmentFirstLeg && WT21FmsUtils.isDiscontinuityLeg(segmentFirstLeg.leg.type); + // We don't need to do this anymore because the activeLegArray is an array of LegDefinition instead of FlightPlanLeg + // WT21FmsUtils.removeFixTypeFlags(activeLegArray); - if (activeLegArray.length === 2) { - if (!discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, FlightPlan.createLeg({ type: LegType.Discontinuity }), 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - if (!WT21FmsUtils.isDiscontinuityLeg(activeLegArray[1].type) || !discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, activeLegArray[1], 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - if (!WT21FmsUtils.isDiscontinuityLeg(activeLegArray[0].type) || !discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, activeLegArray[0], 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } + // If we didn't remove a duplicate, insert a discontinuity at the start of the approach + if (!deduplicatedArrivalLeg && (!prevLeg || !WT21FmsUtils.isVectorsLeg(prevLeg.leg.type))) { + this.tryInsertDiscontinuity(plan, segmentIndex); + } - plan.setLateralLeg(apprSegment.offset + 1); - } else if (activeLegArray.length === 3) { - if (!discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, FlightPlan.createLeg({ type: LegType.Discontinuity }), 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } + this.generateSegmentVerticalData(plan, segmentIndex); - // We should really never get into the scenario where this leg is a discontinuity, but let's be safe - if (!WT21FmsUtils.isDiscontinuityLeg(activeLegArray[2].type) || !discontinuityAlreadyPresent) { - plan.addLeg(segmentIndex, activeLegArray[2], 0, WT21LegDefinitionFlags.DisplacedActiveLeg); - } - - this.createDirectTo(segmentIndex, 0); + if (activeLegArray) { + this.displaceActiveLegsIntoSegment(plan, segmentIndex, activeLegArray, false); } } - this.setVerticalData(plan, segmentIndex); - this.cleanupLegsAfterApproach(plan); this.tryConnectProcedures(plan); @@ -2302,6 +2315,19 @@ export class WT21Fms { return insertProcedureObject; } + /** + * Manages the altitude constraints in a segment when adding a procedure by creating a VerticalData object for each leg. + * @param plan The Flight Plan. + * @param segmentIndex The segment index for the inserted procedure. + */ + private generateSegmentVerticalData(plan: FlightPlan, segmentIndex: number): void { + const segment = plan.getSegment(segmentIndex); + + for (let l = 0; l < segment.legs.length; l++) { + this.generateLegVerticalData(plan, segmentIndex, l); + } + } + /** * Inserts vectors-to-final legs into an insert procedure object. Vectors to final legs consist of a discontinuity * leg followed by a CF leg to the final approach fix. The course of the CF leg (the vectors-to-final course) is @@ -2395,31 +2421,34 @@ export class WT21Fms { } /** - * Manages the altitude constraints when adding a procedure by creating a VerticalData object for each leg. + * Manages the altitude constraints for a leg when adding a procedure by creating a VerticalData object for the leg. * @param plan The Flight Plan. - * @param segmentIndex The segment index for the inserted procedure. + * @param segmentIndex The segment index. + * @param localLegIndex The local leg index. + * @param forceVerticalFlightPhase The vertical flight phase to force on the vertical data. Otherwise, determined by the leg segment type. */ - private setVerticalData(plan: FlightPlan, segmentIndex: number): void { + private generateLegVerticalData(plan: FlightPlan, segmentIndex: number, localLegIndex: number, forceVerticalFlightPhase?: VerticalFlightPhase): void { const segment = plan.getSegment(segmentIndex); - for (let l = 0; l < segment.legs.length; l++) { - const leg = segment.legs[l]; - const altitude1 = leg.leg.altitude1; - const altitude2 = leg.leg.altitude2; - const altDesc = (BitFlags.isAll(leg.leg.fixTypeFlags, FixTypeFlags.MAP) && altitude1 !== 0) ? AltitudeRestrictionType.At : leg.leg.altDesc; - const speedRestriction = leg.leg.speedRestriction; - const verticalData: Partial = { - phase: segment.segmentType === FlightPlanSegmentType.Departure || BitFlags.isAll(leg.flags, LegDefinitionFlags.MissedApproach) - ? VerticalFlightPhase.Climb - : VerticalFlightPhase.Descent, - altDesc: altDesc, - altitude1: altitude1, - altitude2: altitude2, - speed: speedRestriction <= 0 ? 0 : speedRestriction, - speedDesc: speedRestriction <= 0 ? SpeedRestrictionType.Unused : SpeedRestrictionType.AtOrBelow, - speedUnit: SpeedUnit.IAS - }; - plan.setLegVerticalData(segmentIndex, l, verticalData); - } + const leg = segment.legs[localLegIndex]; + + const altitude1 = leg.leg.altitude1; + const altitude2 = leg.leg.altitude2; + const altDesc = (BitFlags.isAll(leg.leg.fixTypeFlags, FixTypeFlags.MAP) && altitude1 !== 0) ? AltitudeRestrictionType.At : leg.leg.altDesc; + const speedRestriction = leg.leg.speedRestriction; + + const verticalData: Partial = { + phase: forceVerticalFlightPhase ?? (segment.segmentType === FlightPlanSegmentType.Departure || BitFlags.isAll(leg.flags, LegDefinitionFlags.MissedApproach) + ? VerticalFlightPhase.Climb + : VerticalFlightPhase.Descent), + altDesc: altDesc, + altitude1: altitude1, + altitude2: altitude2, + speed: speedRestriction <= 0 ? 0 : speedRestriction, + speedDesc: speedRestriction <= 0 ? SpeedRestrictionType.Unused : SpeedRestrictionType.AtOrBelow, + speedUnit: SpeedUnit.IAS + }; + + plan.setLegVerticalData(segmentIndex, localLegIndex, verticalData); } /** @@ -2599,6 +2628,10 @@ export class WT21Fms { plan.setDeparture(); this.planClearSegment(segmentIndex, FlightPlanSegmentType.Departure); + + // Remove constraints from first enroute leg + this.clearFirstEnrouteLegVerticalData(plan); + if (plan.originAirport) { const airport = await this.facLoader.getFacility(FacilityType.Airport, plan.originAirport); const updatedSegmentIndex = this.ensureOnlyOneSegmentOfType(FlightPlanSegmentType.Departure); @@ -2623,20 +2656,23 @@ export class WT21Fms { plan.setArrival(); - const activeLegArray = this.getActiveLegsInCurrentProcedure(plan, segmentIndex); + const activeLegArray = this.getSegmentActiveLegsToDisplace(plan, segmentIndex); this.cleanupLegsAfterApproach(plan); this.planRemoveSegment(segmentIndex); + // Remove constraints from last enroute leg + this.clearLastEnrouteLegVerticalData(plan); + const prevLeg = plan.getPrevLeg(segmentIndex, 0); const nextLeg = plan.getNextLeg(segmentIndex, -1); if (prevLeg && nextLeg && this.isDuplicateLeg(prevLeg.leg, nextLeg.leg)) { this.planRemoveDuplicateLeg(prevLeg, nextLeg); } - if (activeLegArray && activeLegArray.length === 2) { - this.addActiveLegsToEnroute(plan, activeLegArray); + if (activeLegArray) { + this.displaceActiveLegsToEnroute(plan, activeLegArray); } plan.calculate(0); @@ -2655,7 +2691,7 @@ export class WT21Fms { const segmentIndex = this.ensureOnlyOneSegmentOfType(FlightPlanSegmentType.Approach); - const activeLegArray = this.getActiveLegsInCurrentProcedure(plan, segmentIndex); + const activeLegArray = this.getSegmentActiveLegsToDisplace(plan, segmentIndex); plan.procedureDetails.arrivalRunwayTransitionIndex = -1; plan.setDestinationRunway(undefined, false); @@ -2665,6 +2701,11 @@ export class WT21Fms { this.planRemoveSegment(segmentIndex); + // Remove constraints from last enroute leg if there wasn't an arrival + if (plan.procedureDetails.arrivalIndex === -1) { + this.clearLastEnrouteLegVerticalData(plan); + } + const prevLeg = plan.getPrevLeg(segmentIndex, 0); const nextLeg = plan.getNextLeg(segmentIndex, -1); if (prevLeg && nextLeg && this.isDuplicateLeg(prevLeg.leg, nextLeg.leg)) { @@ -2672,20 +2713,66 @@ export class WT21Fms { } if (activeLegArray) { - WT21FmsUtils.removeDisplacedActiveLegs(plan); - this.addActiveLegsToEnroute(plan, activeLegArray, true); + this.displaceActiveLegsToEnroute(plan, activeLegArray, true); } plan.calculate(0); } /** - * Adds active leg pair to the last enroute segment when a procedure is deleted and the current activeLateralLeg is in that procedure. - * @param plan The FlightPlan. - * @param activeLegArray The Active Leg Pair. + * Clears the vertical data of the last enroute leg, if applicable + * + * @param plan the lateral flight plan + */ + private clearFirstEnrouteLegVerticalData(plan: FlightPlan): void { + let firstEnrouteSegment: FlightPlanSegment | undefined; + for (let i = 0; i < plan.segmentCount; i++) { + const segment = plan.getSegment(i); + + if (segment.segmentType === FlightPlanSegmentType.Enroute && segment.legs.length > 0) { + firstEnrouteSegment = segment; + break; + } + } + + if (firstEnrouteSegment) { + plan.setLegVerticalData(firstEnrouteSegment.offset, { altDesc: AltitudeRestrictionType.Unused, speedDesc: SpeedRestrictionType.Unused }); + } + } + + /** + * Clears the vertical data of the last enroute leg, if applicable + * @param plan the lateral flight plan + */ + private clearLastEnrouteLegVerticalData(plan: FlightPlan): void { + let lastEnrouteSegment: FlightPlanSegment | undefined; + for (let i = plan.segmentCount - 1; i >= 0; i--) { + const segment = plan.getSegment(i); + + if (segment.segmentType === FlightPlanSegmentType.Enroute && segment.legs.length > 0) { + lastEnrouteSegment = segment; + break; + } + } + + if (lastEnrouteSegment) { + plan.setLegVerticalData( + lastEnrouteSegment.offset + (lastEnrouteSegment.legs.length - 1), + { altDesc: AltitudeRestrictionType.Unused, speedDesc: SpeedRestrictionType.Unused }, + ); + } + } + + /** + * Displaces a sequence of flight plan legs contained in a now-removed segment ending with the active leg into the + * end of the last enroute flight plan segment. If the displaced active leg was a direct-to leg, then a new direct-to + * will be created to the displaced target leg. Otherwise, the active leg is set to the displaced active leg. + * @param plan The flight plan into which to displace the legs. + * @param activeLegArray The sequence of flight plan legs to displace. The sequence should contain all of the legs + * contained in the former active segment up to and including the active leg. * @param checkForArrivalSegment Whether to check first for an arrival segment to add the legs to. */ - private addActiveLegsToEnroute(plan: FlightPlan, activeLegArray: FlightPlanLeg[], checkForArrivalSegment = false): void { + private displaceActiveLegsToEnroute(plan: FlightPlan, activeLegArray: LegDefinition[], checkForArrivalSegment = false): void { let segmentIndex = this.findLastEnrouteSegmentIndex(plan); if (checkForArrivalSegment && plan.procedureDetails.arrivalIndex > -1) { const arrivalSegmentIndex = this.ensureOnlyOneSegmentOfType(FlightPlanSegmentType.Arrival, false); @@ -2694,26 +2781,7 @@ export class WT21Fms { } } - const segment = plan.getSegment(segmentIndex); - - if (activeLegArray.length === 2) { - plan.addLeg(segmentIndex, activeLegArray[0], undefined, WT21LegDefinitionFlags.DisplacedActiveLeg); - plan.addLeg(segmentIndex, activeLegArray[1], undefined, WT21LegDefinitionFlags.DisplacedActiveLeg); - this.planAddLeg(segmentIndex, FlightPlan.createLeg({ - type: LegType.Discontinuity - }), undefined, WT21LegDefinitionFlags.DisplacedActiveLeg); - - plan.setLateralLeg(segment.offset + segment.legs.length - 2); - - } else if (activeLegArray.length === 3) { - plan.addLeg(segmentIndex, activeLegArray[1]); - plan.addLeg(segmentIndex, activeLegArray[2], WT21LegDefinitionFlags.DisplacedActiveLeg); - this.planAddLeg(segmentIndex, FlightPlan.createLeg({ - type: LegType.Discontinuity - })); - this.createDirectTo(segmentIndex, segment.legs.length - 2); - } - + this.displaceActiveLegsIntoSegment(plan, segmentIndex, activeLegArray, true); } /** @@ -3138,6 +3206,7 @@ export class WT21Fms { directLeg.type = legType; directLeg.course = course as number; directLeg.trueDegrees = false; + directLeg.turnDirection = LegTurnDirection.None; return directLeg; } else { return FlightPlan.createLeg({ @@ -3147,21 +3216,6 @@ export class WT21Fms { trueDegrees: false }); } - - - // const planeHeading = SimVar.GetSimVarValue('PLANE HEADING DEGREES TRUE', 'degrees'); - // if (leg) { - // const directLeg = Object.assign({}, leg); - // directLeg.type = LegType.DF; - // directLeg.course = planeHeading === 0 ? 360 : planeHeading; - // return directLeg; - // } else { - // return FlightPlan.createLeg({ - // type: LegType.DF, - // fixIcao: icao, - // course: planeHeading === 0 ? 360 : planeHeading - // }); - // } } /** @@ -3179,6 +3233,7 @@ export class WT21Fms { return; } // We need to recreate the DTO so that the proper events get sent and legs get recreated and what not + // We do not mark it as pending, because that is the decision of whatever created the original DTO this.createDirectTo(plan.directToData.segmentIndex, plan.directToData.segmentLegIndex, false); } @@ -3321,7 +3376,6 @@ export class WT21Fms { plan.addSegment(0, FlightPlanSegmentType.Departure); plan.addSegment(1, FlightPlanSegmentType.Enroute); - // plan.addSegment(2, FlightPlanSegmentType.Destination); plan.removeOriginAirport(); plan.removeDestinationAirport(); @@ -3887,18 +3941,6 @@ export class WT21Fms { const dtoLegIndex = plan.directToData.segmentLegIndex; const dtoSegmentIndex = plan.directToData.segmentIndex; - // TODO Removed the segmentIndex < dtoSegmentIndex from Garmin as it does not apply to WT21 (I think, but want to verify) - - // if ( - // dtoSegmentIndex >= 0 - // && ( - // segmentIndex < dtoSegmentIndex - // || (segmentIndex === dtoSegmentIndex && index !== undefined && index <= dtoLegIndex) - // ) - // ) { - // this.removeDirectToExisting(plan.planIndex); - // } - if ( dtoSegmentIndex >= 0 && (segmentIndex === dtoSegmentIndex && index !== undefined && index <= dtoLegIndex) @@ -4468,7 +4510,9 @@ export class WT21Fms { || leg1.type === LegType.TF || leg1.type === LegType.DF || leg1.type === LegType.CF) - && leg1.fixIcao === leg2.fixIcao; + && ICAO.getRegionCode(leg1.fixIcao) === ICAO.getRegionCode(leg2.fixIcao) + && ICAO.getIdent(leg1.fixIcao) === ICAO.getIdent(leg2.fixIcao) + && ICAO.getFacilityType(leg1.fixIcao) === ICAO.getFacilityType(leg2.fixIcao); } /** @@ -4493,7 +4537,9 @@ export class WT21Fms { return false; } - return leg1.fixIcao === leg2.fixIcao; + return ICAO.getRegionCode(leg1.fixIcao) === ICAO.getRegionCode(leg2.fixIcao) + && ICAO.getIdent(leg1.fixIcao) === ICAO.getIdent(leg2.fixIcao) + && ICAO.getFacilityType(leg1.fixIcao) === ICAO.getFacilityType(leg2.fixIcao); } /** diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlan/WT21FmsUtils.ts b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlan/WT21FmsUtils.ts index 5cdc008a4..226eac8e7 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlan/WT21FmsUtils.ts +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlan/WT21FmsUtils.ts @@ -902,6 +902,15 @@ export class WT21FmsUtils { return discontinuityLegTypes.includes(legType); } + /** + * Checks if leg type is a "vectors" leg type. + * @param legType The LegType. + * @returns Whether the leg type is a "vectors" leg type. + */ + public static isVectorsLeg(legType: LegType): boolean { + return vectorsTypes.includes(legType); + } + /** * Checks if leg type is a course or heading leg, * which should have the leg course shown instead of the initial dtk. @@ -1396,3 +1405,6 @@ const discontinuityLegTypes = [LegType.Discontinuity, LegType.ThruDiscontinuity] /** Leg types where the leg course should be shown instead of the initial dtk. */ const showCourseLegTypes = [LegType.CA, LegType.CD, LegType.CF, LegType.CI, LegType.CR, LegType.FM, LegType.VA, LegType.VD, LegType.VI, LegType.VM, LegType.VR] as readonly LegType[]; + +/** Array of "vectors" leg types */ +const vectorsTypes = [LegType.FM, LegType.VM]; diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlanAsoboSync.ts b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlanAsoboSync.ts index 571f73302..f62147137 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlanAsoboSync.ts +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/FlightPlanAsoboSync.ts @@ -105,13 +105,6 @@ export class FlightPlanAsoboSync { FlightPlanAsoboSync.buildNonAirportDestLeg(data, plan, fms, lastEnrouteSegment); } - - // if (destinationSet && !originSet) { - // if (plan.length >= 1) { - // plan.getSegmentIndex(0) - // fms.createDirectToExisting() - // } - // } plan.calculate(0).then(() => { plan.setLateralLeg(0); }); @@ -199,6 +192,16 @@ export class FlightPlanAsoboSync { await Coherent.call('TRY_AUTOACTIVATE_APPROACH').catch((err: any) => console.log(JSON.stringify(err))); } + try { + const currCrzAlt: number = await Coherent.call('GET_CRUISE_ALTITUDE').catch((err: any) => console.log(JSON.stringify(err))); + const desiredCrzAlt: number = fms.activePerformancePlan.cruiseAltitude.get() ?? -1; + if (desiredCrzAlt > -1 && (currCrzAlt === -1 || currCrzAlt < desiredCrzAlt)) { + await Coherent.call('SET_CRUISE_ALTITUDE', desiredCrzAlt).catch((err: any) => console.log(JSON.stringify(err))); + } + } catch (error) { + console.warn('Error setting cruise altitude: ' + error); + } + Coherent.call('RECOMPUTE_ACTIVE_WAYPOINT_INDEX').catch((err: any) => console.log(JSON.stringify(err))); } catch (error) { console.error(`Error during fp sync: ${error}`); diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/HSI/MfdHsi.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/HSI/MfdHsi.tsx index dc7b6867f..fb8305b66 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/HSI/MfdHsi.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/HSI/MfdHsi.tsx @@ -1,23 +1,16 @@ -import { - CombinedSubject, - ComponentProps, - DisplayComponent, - EventBus, - FlightPlanner, - FSComponent, - Subject, - VNode -} from '@microsoft/msfs-sdk'; +import { ComponentProps, DisplayComponent, EventBus, FlightPlanner, FSComponent, MappedSubject, Subject, VNode } from '@microsoft/msfs-sdk'; + import { MfdDisplayMode, MFDUserSettings } from '../../../MFD/MFDUserSettings'; import { WT21TCAS } from '../../Traffic/WT21TCAS'; import { LeftInfoPanel } from '../LeftInfoPanel/LeftInfoPanel'; import { RightInfoPanel } from '../RightInfoPanel/RightInfoPanel'; import { WaypointAlerter } from '../WaypointAlerter'; import { HSIContainer } from './HSIContainer'; - -import './MfdHsi.css'; import { WT21FixInfoManager } from '../../../FMC/Systems/WT21FixInfoManager'; import { PerformancePlan } from '../../Performance/PerformancePlan'; +import { WT21DisplayUnitFsInstrument } from '../../WT21DisplayUnitFsInstrument'; + +import './MfdHsi.css'; /** * The properties for the MfdHsi component. @@ -26,6 +19,9 @@ interface MfdHsiProps extends ComponentProps { /** An instance of the event bus. */ bus: EventBus; + /** The display unit */ + displayUnit: WT21DisplayUnitFsInstrument; + /** An instance of the flight planner. */ flightPlanner: FlightPlanner; @@ -88,7 +84,7 @@ export class MfdHsi extends DisplayComponent { } }, true); - CombinedSubject.create(this.visibleForElectricity, this.visibleForMode).sub(([visibleForElectricity, visibleForMode]) => { + MappedSubject.create(this.visibleForElectricity, this.visibleForMode).sub(([visibleForElectricity, visibleForMode]) => { const shown = visibleForElectricity && visibleForMode; this.containerRef.instance.classList.toggle('hidden', !shown); @@ -101,7 +97,7 @@ export class MfdHsi extends DisplayComponent { <>
- +
{ />
- +
diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/LeftInfoPanel/ElapsedTimeDisplay.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/LeftInfoPanel/ElapsedTimeDisplay.css index 207509a9a..796de56d5 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/LeftInfoPanel/ElapsedTimeDisplay.css +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/LeftInfoPanel/ElapsedTimeDisplay.css @@ -1,11 +1,25 @@ .hsi-elapsed-time { position: absolute; top: 516px; + display: flex; flex-direction: row; + align-items: center; + font-size: 22px; } +.hsi-elapsed-time.hsi-elapsed-time-side-buttons { + top: 506px; +} + +.hsi-elapsed-time .hsi-et-arrow { + min-width: 10px; + height: 14px; + margin-bottom: 1px; + margin-right: 10px; +} + .hsi-elapsed-time .hsi-et-label { color: var(--wt21-colors-white); } @@ -21,6 +35,20 @@ white-space: nowrap; } +.hsi-elapsed-time.hsi-elapsed-time-side-buttons .hsi-et-value { + display: flex; + flex-direction: row; + justify-content: flex-end; + + line-height: 19px; + padding-top: 6px; + padding-left: 2px; + margin-bottom: 5px; + margin-left: 14px; + + background-color: var(--wt21-colors-black); +} + .hsi-elapsed-time .hsi-et-value-colon { margin: 0 -4px; } \ No newline at end of file diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/LeftInfoPanel/ElapsedTimeDisplay.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/LeftInfoPanel/ElapsedTimeDisplay.tsx index 52f2e6de1..6115cca2d 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/LeftInfoPanel/ElapsedTimeDisplay.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/LeftInfoPanel/ElapsedTimeDisplay.tsx @@ -1,4 +1,5 @@ -import { ComponentProps, DisplayComponent, FSComponent, Subscribable, VNode } from '@microsoft/msfs-sdk'; +import { ComponentProps, DisplayComponent, FSComponent, Subject, Subscribable, VNode } from '@microsoft/msfs-sdk'; +import { DisplayUnitLayout } from '../../Config/DisplayUnitConfig'; import './ElapsedTimeDisplay.css'; @@ -8,25 +9,42 @@ interface ElapsedTimeProps extends ComponentProps { elapsedTimeText: Subscribable; // eslint-disable-next-line jsdoc/require-jsdoc isVisible: Subscribable; + // eslint-disable-next-line jsdoc/require-jsdoc + displayUnitLayout: DisplayUnitLayout; } /** The ElapsedTime which is displayed in the bottom left of the PFD when active. */ export class ElapsedTimeDisplay extends DisplayComponent { private readonly elapsedTimeRef = FSComponent.createRef(); + private readonly hidden = Subject.create(true); + /** @inheritdoc */ public onAfterRender(): void { - this.props.isVisible.sub(isVisible => { - this.elapsedTimeRef.instance.classList.toggle('hidden', !isVisible); - }); + this.props.isVisible.sub(isVisible => this.hidden.set(!isVisible)); } /** @inheritdoc */ public render(): VNode { + const isUsingSoftkeys = this.props.displayUnitLayout === DisplayUnitLayout.Softkeys; + return ( -
- +
); diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/FormatInfo.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/FormatInfo.css new file mode 100644 index 000000000..a1bc53370 --- /dev/null +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/FormatInfo.css @@ -0,0 +1,55 @@ +.info-format-switch { + display: flex; + flex-flow: row nowrap; + align-items: center; + + text-align: right; +} + +.info-format-switch.info-format-switch-right { + flex-flow: row-reverse nowrap; +} + +.info-format-switch:not(.info-format-switch-right) { + position: relative; + top: 215px; /* TODO this should go elsewhere */ +} + +.info-format-switch-switch-arrow-wrapper { + height: 0; +} + +.info-format-switch:not(.info-format-switch-right) .info-format-switch-switch-arrow { + min-width: 10px; + height: 14px; + margin-bottom: 6px; + margin-right: 10px; +} + +.info-format-switch-right .info-format-switch-switch-arrow { + min-width: 10px; + height: 14px; + margin-bottom: 6px; + margin-left: 10px; +} + +.info-format-switch-label { + white-space: pre; + display: flex; + flex-direction: row; + justify-content: flex-end; + + line-height: 19px; + padding-top: 6px; + padding-left: 2px; + + margin-bottom: 5px; + color: var(--wt21-colors-cyan); + background-color: var(--wt21-colors-black); +} + +.info-format-switch:not(.info-format-switch-right) .info-format-switch-label { + right: unset; + + justify-content: flex-start; +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/FormatSwitch.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/FormatSwitch.tsx new file mode 100644 index 000000000..a1609e8cf --- /dev/null +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/FormatSwitch.tsx @@ -0,0 +1,57 @@ +import { ComponentProps, DisplayComponent, EventBus, FSComponent, Subject, Subscribable, VNode } from '@microsoft/msfs-sdk'; + +import { MFDUserSettings } from '../../../MFD/MFDUserSettings'; +import { WT21DisplayUnitFsInstrument, WT21DisplayUnitType } from '../../WT21DisplayUnitFsInstrument'; + +import './FormatInfo.css'; + +/** + * Props for {@link FormatSwitch} + */ +export interface FormatInfoProps extends ComponentProps { + /** The event bus */ + bus: EventBus, + + /** The display unit */ + displayUnit: WT21DisplayUnitFsInstrument, + + /** The format to control */ + format: 'upper' | 'lower', + + /** The orientation of the label */ + orientation: 'left' | 'right', +} + +/** + * FORMAT switch + */ +export class FormatSwitch extends DisplayComponent { + private static readonly ARROW_ORIENTATIONS: Record<'left' | 'right', string> = { + left: 'M 7, 2 l -4, 5 l 4, 5', + right: 'M 3, 2 l 4, 5 l -4, 5', + }; + + private readonly userSettingsManagerMfd = MFDUserSettings.getAliasedManager(this.props.bus); + private readonly mfdSoftkeyFormatChangeActiveSetting = this.userSettingsManagerMfd.getSetting('mfdSoftkeyFormatChangeActive'); + + private readonly formatText = this.props.displayUnit.displayUnitType === WT21DisplayUnitType.Pfd ? 'FORMAT' : (this.props.format === 'upper' ? 'UPPER FORMAT' : 'LOWER FORMAT'); + + private readonly showLabel: Subscribable = this.props.displayUnit.displayUnitType === WT21DisplayUnitType.Pfd + ? Subject.create(true) + : this.mfdSoftkeyFormatChangeActiveSetting; + + private readonly formatLabel = this.showLabel.map((show) => show ? this.formatText : '\u00a0'); + + /** @inheritDoc */ + public render(): VNode | null { + return ( +
+ + + + + {this.formatLabel} +
+ ); + } +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/MinimumsDisplay.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/MinimumsDisplay.css index a67dc4ea4..dd44118cb 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/MinimumsDisplay.css +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/MinimumsDisplay.css @@ -1,7 +1,7 @@ .minimums-display-container { position: absolute; - top: 6%; - right: -4%; + top: -250px; + right: -28px; width: 185px; font-size: 24px; text-align: left; @@ -9,6 +9,11 @@ word-spacing: -.6ch; } +.right-info-panel-side-buttons > .minimums-display-container { + top: -206px; + right: -28px; +} + .minimums-display-container.alert { color: var(--wt21-colors-yellow); animation: mins-blink .75s step-start 5; diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/RightInfoPanel.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/RightInfoPanel.css index ff9f6a77b..642c0989a 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/RightInfoPanel.css +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/RightInfoPanel.css @@ -1,7 +1,12 @@ .right-info-panel { position: relative; + top: 39.5%; /* TODO This should be temporary. */ /* TODO Remove once the geometry for the panel has been fixed to leave gaps around the panel. */ margin-right: 4px; height: 100%; -} \ No newline at end of file +} + +.right-info-panel-side-buttons { + top: 38%; +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/RightInfoPanel.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/RightInfoPanel.tsx index 6eea6ea06..97bf99c67 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/RightInfoPanel.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/RightInfoPanel.tsx @@ -1,11 +1,12 @@ import { ComponentProps, DisplayComponent, EventBus, FSComponent, VNode } from '@microsoft/msfs-sdk'; - -import { PfdOrMfd } from '../../Map/MapUserSettings'; import { WT21TCAS } from '../../Traffic/WT21TCAS'; import { MinimumsDisplay } from './MinimumsDisplay'; import { NextradInfo } from './NextradInfo'; import { TerrWxInfo } from './TerrWxInfo'; import { TfcInfo } from './TfcInfo'; +import { WT21DisplayUnitFsInstrument, WT21DisplayUnitType } from '../../WT21DisplayUnitFsInstrument'; +import { DisplayUnitLayout } from '../../Config/DisplayUnitConfig'; +import { FormatSwitch } from './FormatSwitch'; import './RightInfoPanel.css'; @@ -14,24 +15,59 @@ interface RightInfoPanelProps extends ComponentProps { /** An instance of the event bus. */ bus: EventBus; + /** The display unit */ + displayUnit: WT21DisplayUnitFsInstrument; + /** The TCAS instance. */ tcas: WT21TCAS; - - /** Whether the component is on the PFD or the MFD. */ - pfdOrMfd: PfdOrMfd; } /** The RightInfoPanel component. */ export class RightInfoPanel extends DisplayComponent { + private readonly isUsingSoftkeys = this.props.displayUnit.displayUnitConfig.displayUnitLayout === DisplayUnitLayout.Softkeys; /** @inheritdoc */ public render(): VNode { return ( -
- - - {this.props.pfdOrMfd === 'MFD' ? : null} - {this.props.pfdOrMfd === 'PFD' ? : null} +
+ {this.isUsingSoftkeys ? ( + <> + + + + + ) : ( + <> + + + + )} + {this.props.displayUnit.displayUnitType === WT21DisplayUnitType.Mfd ? : null} + {this.props.displayUnit.displayUnitType === WT21DisplayUnitType.Pfd ? : null}
); } diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TerrWxInfo.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TerrWxInfo.css index a902ebc42..78f48a792 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TerrWxInfo.css +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TerrWxInfo.css @@ -1,6 +1,4 @@ .terr-wx-info { - position: absolute; - top: 73.65%; right: 0; color: var(--wt21-colors-white); text-align: right; @@ -8,6 +6,10 @@ letter-spacing: -1.75px; } +.terr-wx-info-side-buttons { + margin-top: 65px; +} + .terr-wx-info > * + * { margin-top: -6px; /* font-size: 26px; */ @@ -49,4 +51,44 @@ .terr-wx-info .line-3 { margin-top: -10px; -} \ No newline at end of file +} + +.right-info-terr-wxr-switch { + letter-spacing: 1.1px; +} + +.right-info-terr-wxr-switch-arrow-wrapper { + height: 0; +} + +.right-info-terr-wxr-switch-arrow { + position: relative; + width: 10px; + height: 14px; + + top: 5px; +} + +.right-info-terr-wxr-switch-info { + display: flex; + flex-flow: column nowrap; + + padding-right: 18px; +} + +.right-info-terr-wxr-switch-line { + color: var(--wt21-colors-white); + + font-size: 20px; + line-height: 21px; +} + +.right-info-terr-wxr-switch-line-selected { + color: var(--wt21-colors-cyan); + + font-size: 21px; +} + +.right-info-terr-wxr-switch-wx { + font-size: 16px; +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TerrWxInfo.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TerrWxInfo.tsx index d5aeb818c..36f1b0811 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TerrWxInfo.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TerrWxInfo.tsx @@ -1,7 +1,9 @@ -import { ComponentProps, DisplayComponent, EventBus, FSComponent, Subject, VNode } from '@microsoft/msfs-sdk'; -import { MapFormatSupportMatrix } from '../../Map/MapFormatSupportMatrix'; +import { ComponentProps, DisplayComponent, EventBus, FSComponent, Subject, UserSettingManager, VNode } from '@microsoft/msfs-sdk'; -import { MapUserSettings, PfdOrMfd, TerrWxState } from '../../Map/MapUserSettings'; +import { DisplayUnitLayout } from '../../Config/DisplayUnitConfig'; +import { MapFormatSupportMatrix } from '../../Map/MapFormatSupportMatrix'; +import { MapSettingsMfdAliased, MapSettingsPfdAliased, MapUserSettings, TerrWxState } from '../../Map/MapUserSettings'; +import { WT21DisplayUnitFsInstrument, WT21DisplayUnitType } from '../../WT21DisplayUnitFsInstrument'; import './TerrWxInfo.css'; @@ -9,18 +11,20 @@ import './TerrWxInfo.css'; interface TerrWxInfoProps extends ComponentProps { /** An instance of the event bus. */ bus: EventBus; - // eslint-disable-next-line jsdoc/require-jsdoc - pfdOrMfd: PfdOrMfd; + + /** The display unit */ + displayUnit: WT21DisplayUnitFsInstrument, } /** The TerrWxInfo component. */ export class TerrWxInfo extends DisplayComponent { - private readonly mapSettingsManager = MapUserSettings.getAliasedManager(this.props.bus, this.props.pfdOrMfd); + private readonly mapSettingsManager = MapUserSettings.getAliasedManager(this.props.bus, this.props.displayUnit.displayUnitType === WT21DisplayUnitType.Pfd ? 'PFD' : 'MFD'); private readonly mapFormatSupport = new MapFormatSupportMatrix(); private readonly terrWxInfoRef = FSComponent.createRef(); private readonly line3 = Subject.create(''); private readonly line4 = Subject.create(''); private readonly line5 = Subject.create(''); + private readonly isUsingSoftkeys = this.props.displayUnit.displayUnitConfig.displayUnitLayout === DisplayUnitLayout.Softkeys; /** @inheritdoc */ public onAfterRender(): void { @@ -57,8 +61,30 @@ export class TerrWxInfo extends DisplayComponent { this.terrWxInfoRef.instance.classList.toggle('format-supported', isSupported); }; - /** @inheritdoc */ - public render(): VNode { + /** @inheritDoc */ + public render(): VNode | null { + return this.isUsingSoftkeys ? this.renderSoftkeyLayout() : this.renderTraditionalLayout(); + } + + /** + * Renders the softkey layout + * + * @returns a vnode + */ + private renderSoftkeyLayout(): VNode { + return ( +
+ +
+ ); + } + + /** + * Renders the traditional layout + * + * @returns a vnode + */ + private renderTraditionalLayout(): VNode { return (
TERR
@@ -70,4 +96,46 @@ export class TerrWxInfo extends DisplayComponent {
); } +} + +/** + * Props for {@link TerrWxSideButtonSwitch} + */ +interface TerrWxSideButtonSwitchProps { + /** The map user settings */ + mapUserSettings: UserSettingManager; +} + +/** + * Renders the TERR/RDR switch + */ +class TerrWxSideButtonSwitch extends DisplayComponent { + private readonly terrWxrState = this.props.mapUserSettings.getSetting('terrWxState'); + + /** @inheritDoc */ + public render(): VNode | null { + return ( +
+
+ + + +
+ +
+
it === 'TERR'), + }}>TERR
+
it === 'WX'), + }}>RDR
+
+ +
STBY
+
T 0.0
+
+ ); + } } \ No newline at end of file diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TfcInfo.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TfcInfo.css index 623900ee6..a595de22c 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TfcInfo.css +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TfcInfo.css @@ -1,9 +1,8 @@ .tfc-info { - position: absolute; - top: 39.25%; - right: 0; - width: 100%; height: 34%; + + margin-top: 39.25%; + color: var(--wt21-colors-white); text-align: right; font-size: 20px; @@ -12,6 +11,31 @@ white-space: nowrap; } +.tfc-info-side-buttons { + margin-top: 12px; + margin-right: 18px; +} + +.right-info-tfc-switch-arrow-wrapper { + height: 0; +} + +.right-info-tfc-switch-arrow { + position: relative; + width: 10px; + height: 14px; + + top: 10px; + right: -18px; +} + +.tfc-info-side-buttons.format-supported .tfc-data { + font-size: 18px; + line-height: 18px; + + color: var(--wt21-colors-white); +} + .tfc-info:not(.tfc-disabled) .tfc-label { font-size: 22px; } @@ -20,6 +44,10 @@ color: var(--wt21-colors-cyan); } +.tfc-info-side-buttons .tfc-label { + line-height: 22px; +} + .tfc-info-hideable { position: absolute; right: 0; @@ -40,6 +68,10 @@ align-items: center; } +.tfc-info-side-buttons .tfc-info-hideable { + top: unset; +} + .tfc-disabled .tfc-info-hideable { display: none; } diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TfcInfo.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TfcInfo.tsx index 809eea17d..f0caee609 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TfcInfo.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/LowerSection/RightInfoPanel/TfcInfo.tsx @@ -8,6 +8,8 @@ import { WT21_PFD_MFD_Colors as WT21_PFD_MFD_Colors } from '../../WT21_Colors'; import { TSSSystemEvents } from '../../Systems'; import { WT21TCAS } from '../../Traffic/WT21TCAS'; import { TrafficSettings, TrafficUserSettings } from '../../Traffic/TrafficUserSettings'; +import { WT21DisplayUnitFsInstrument } from '../../WT21DisplayUnitFsInstrument'; +import { DisplayUnitLayout } from '../../Config/DisplayUnitConfig'; import './TfcInfo.css'; @@ -16,6 +18,9 @@ interface TfcInfoProps extends ComponentProps { /** An instance of the event bus. */ bus: EventBus; + /** The display unit */ + displayUnit: WT21DisplayUnitFsInstrument; + /** The TCAS instance. */ tcas: WT21TCAS; @@ -34,6 +39,8 @@ export class TfcInfo extends DisplayComponent { private readonly mapSettings = MapUserSettings.getAliasedManager(this.props.bus, this.props.pfdOrMfd); private readonly trafficSettings = TrafficUserSettings.getManager(this.props.bus); + private readonly isUsingSoftkeys = this.props.displayUnit.displayUnitConfig.displayUnitLayout === DisplayUnitLayout.Softkeys; + private readonly isVisible = MappedSubject.create( ([format, isTfcEnabled]): boolean => { return format === 'TCAS' || isTfcEnabled; @@ -55,8 +62,34 @@ export class TfcInfo extends DisplayComponent { this.rootRef.instance.classList.toggle('format-supported', format !== 'PLAN'); }; + /** @inheritDoc */ + public render(): VNode | null { + return this.isUsingSoftkeys ? this.renderSoftkeyLayout() : this.renderTraditionalLayout(); + } + /** @inheritdoc */ - public render(): VNode { + private renderSoftkeyLayout(): VNode { + return ( +
+
+ + + +
+ +
TFC
+
+ + + + +
+
+ ); + } + + /** @inheritdoc */ + private renderTraditionalLayout(): VNode { return (
TFC
@@ -85,7 +118,7 @@ export class TfcInfo extends DisplayComponent { /** * Component props for individual TfcInfo subfields. */ -interface TfcInfoFieldProps extends TfcInfoProps { +interface TfcInfoFieldProps extends Omit { /** The traffic user settings manager. */ trafficSettings: UserSettingManager; diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/Checkbox.css b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/Checkbox.css index 85871380b..901be2a14 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/Checkbox.css +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/Checkbox.css @@ -1,7 +1,36 @@ +.check-side-buttons-arrow-wrapper { + display: inline; + + width: 0; + height: 0; +} + +.check-side-buttons-arrow { + position: relative; + + width: 10px; + height: 14px; + + left: -10px; +} + +.popup-menu-checkbox-right .check-side-buttons-arrow { + left: unset; + right: -12px; +} + .check-label { display: block; } +.popup-menu-checkbox-side-button .check-label { + margin-left: 18px; +} + +.popup-menu-checkbox-side-button.popup-menu-checkbox-right .check-label { + margin-right: 18px; +} + .check-design { display: inline-block; position: relative; @@ -40,16 +69,41 @@ stroke: var(--wt21-colors-magenta); } -.popup-menu-checkbox label .check-text { +.popup-menu-checkbox:not(.popup-menu-checkbox-side-button) label .check-text { width: calc(100% - 25px); display: inline-flex; justify-content: space-between } +.popup-menu-checkbox.popup-menu-checkbox-side-button label .check-text { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.popup-menu-checkbox.popup-menu-checkbox-side-button.popup-menu-checkbox-right label .check-text { + align-items: flex-end; +} + .popup-menu-checkbox.disabled label .check-text { color: var(--wt21-colors-dark-gray); } +.popup-menu-checkbox-side-button .check-text { + flex-flow: column nowrap; +} + .check-select-value { padding-right: 3px; -} \ No newline at end of file +} + +.popup-menu-checkbox-side-button .check-select-value { + padding-top: 10px; + font-size: 30px; + + border: 2px solid transparent; +} + +.popup-menu-checkbox-side-button .check-select-value.highlight { + border: 2px solid var(--wt21-colors-cyan); +} diff --git a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/CheckboxNumeric.tsx b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/CheckboxNumeric.tsx index f0e47ce1e..8a447a6d6 100644 --- a/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/CheckboxNumeric.tsx +++ b/src/workingtitle-instruments-wt21/html_ui/Pages/VCockpit/Instruments/WT21/Shared/Menus/Components/CheckboxNumeric.tsx @@ -1,9 +1,17 @@ -import { FSComponent, MathUtils, MutableSubscribable, Subject, Subscribable, VNode } from '@microsoft/msfs-sdk'; +import { FSComponent, MathUtils, MutableSubscribable, Subject, Subscribable, SubscribableUtils, VNode } from '@microsoft/msfs-sdk'; import { WT21UiControl, WT21UiControlProps } from '../../UI/WT21UiControl'; import './Checkbox.css'; +/** + * Style of {@link CheckBoxNumeric} + */ +export enum CheckBoxNumericStyle { + Inline, + SideButtonBound, +} + /** * The properties for the CheckBox component. */ @@ -12,7 +20,7 @@ interface CheckBoxNumericProps extends WT21UiControlProps { label: string; /** The data ref subject for whether the checkbox is selected. */ - checkedDataRef: MutableSubscribable; + checkedDataRef: Subscribable | MutableSubscribable; /** The data ref subject for the selected value. */ dataRef: MutableSubscribable; @@ -29,6 +37,12 @@ interface CheckBoxNumericProps extends WT21UiControlProps { /** The maxmimun number of the value */ max?: number | Subscribable; + /** Style of numeric checkbox */ + style?: CheckBoxNumericStyle; + + /** Orientation of the numeric checkbox - only relevant if {@link style} is set to `SideButtonBound`. */ + orientation?: 'left' | 'right'; + /** Handler being fired when the checked status changes by control input */ onCheckedChanged?(value: boolean, sender: CheckBoxNumeric): void; @@ -40,20 +54,26 @@ interface CheckBoxNumericProps extends WT21UiControlProps { * The CheckBox component. */ export class CheckBoxNumeric extends WT21UiControl { + private static readonly ARROW_ORIENTATIONS: Record<'left' | 'right', string> = { + left: 'M 7, 2 l -4, 5 l 4, 5', + right: 'M 3, 2 l 4, 5 l -4, 5', + }; - protected readonly el = FSComponent.createRef(); + protected readonly focusEl = FSComponent.createRef(); protected readonly inputRef = FSComponent.createRef(); protected readonly uncheckOnChange = Subject.create(this.props.uncheckOnChange !== undefined ? this.props.uncheckOnChange : true); protected readonly increments = typeof this.props.increments === 'object' ? this.props.increments : Subject.create(this.props.increments !== undefined ? this.props.increments : 1); protected readonly min = typeof this.props.min === 'object' ? this.props.min : Subject.create(this.props.min !== undefined ? this.props.min : 0); protected readonly max = typeof this.props.max === 'object' ? this.props.max : Subject.create(this.props.max !== undefined ? this.props.max : 99999); + protected readonly style = this.props.style ?? CheckBoxNumericStyle.Inline; /** @inheritdoc */ public onUpperKnobPush(): boolean { - if (this.isDisabled === false) { - this.props.checkedDataRef.set(!this.props.checkedDataRef.get()); + if (!this.isDisabled) { if (this.props.onCheckedChanged) { - this.props.onCheckedChanged(this.props.checkedDataRef.get(), this); + this.props.onCheckedChanged(!this.props.checkedDataRef.get(), this); + } else { + SubscribableUtils.isMutableSubscribable(this.props.checkedDataRef) && this.props.checkedDataRef.set(!this.props.checkedDataRef.get()); } } return true; @@ -61,7 +81,7 @@ export class CheckBoxNumeric extends WT21UiControl { /** @inheritdoc */ public onUpperKnobInc(): boolean { - if (this.isDisabled === false) { + if (!this.isDisabled) { this.props.dataRef.set(MathUtils.clamp(this.props.dataRef.get() + this.increments.get(), this.min.get(), this.max.get())); this.onValueChangedInternal(); } @@ -70,7 +90,7 @@ export class CheckBoxNumeric extends WT21UiControl { /** @inheritdoc */ public onUpperKnobDec(): boolean { - if (this.isDisabled === false) { + if (!this.isDisabled) { this.props.dataRef.set(MathUtils.clamp(this.props.dataRef.get() - this.increments.get(), this.min.get(), this.max.get())); this.onValueChangedInternal(); } @@ -81,7 +101,9 @@ export class CheckBoxNumeric extends WT21UiControl { * Handles value changes by user input. */ private onValueChangedInternal(): void { - if (this.isDisabled === false && this.uncheckOnChange.get()) { this.props.checkedDataRef.set(false); } + if (!this.isDisabled && this.uncheckOnChange.get()) { + SubscribableUtils.isMutableSubscribable(this.props.checkedDataRef) && this.props.checkedDataRef.set(false); + } if (this.props.onValueChanged) { this.props.onValueChanged(this.props.dataRef.get(), this); } @@ -89,12 +111,12 @@ export class CheckBoxNumeric extends WT21UiControl { /** @inheritdoc */ protected onFocused(): void { - this.el.instance.classList.add(WT21UiControl.FOCUS_CLASS); + this.focusEl.instance.classList.add(WT21UiControl.FOCUS_CLASS); } /** @inheritdoc */ protected onBlurred(): void { - this.el.instance.classList.remove(WT21UiControl.FOCUS_CLASS); + this.focusEl.instance.classList.remove(WT21UiControl.FOCUS_CLASS); } /** @inheritdoc */ @@ -115,13 +137,13 @@ export class CheckBoxNumeric extends WT21UiControl { }, true); this.min.sub(min => { - if (this.isDisabled === false && min > this.props.dataRef.get()) { + if (!this.isDisabled && min > this.props.dataRef.get()) { this.props.dataRef.set(min); } }); this.max.sub(max => { - if (this.isDisabled === false && max < this.props.dataRef.get()) { + if (!this.isDisabled && max < this.props.dataRef.get()) { this.props.dataRef.set(max); } }); @@ -131,11 +153,21 @@ export class CheckBoxNumeric extends WT21UiControl { /** @inheritdoc */ public render(): VNode { return ( -