├── .gitignore ├── kln90b ├── Version.ts ├── controls │ ├── Inverted.tsx │ ├── selects │ │ ├── NdbSelector.tsx │ │ ├── AirportSelector.tsx │ │ ├── SupplementarySelector.tsx │ │ ├── IntersectionSelector.tsx │ │ ├── VorSelector.tsx │ │ ├── VolumeFieldset.tsx │ │ ├── CreateWaypointMessage.tsx │ │ ├── SuperNav5RangeSelector.tsx │ │ ├── BearingFieldset.tsx │ │ ├── TempFieldset.tsx │ │ ├── SpeedFieldset.tsx │ │ ├── SelectField.tsx │ │ ├── TimeFieldset.tsx │ │ ├── FpmFieldset.tsx │ │ ├── MapOrientationSelector.tsx │ │ └── NearestSelector.tsx │ ├── displays │ │ ├── TextDisplay.tsx │ │ ├── TemperatureDisplay.tsx │ │ ├── TimeDisplay.tsx │ │ ├── AltitudeDisplay.tsx │ │ ├── LatitudeDisplay.tsx │ │ ├── LongitudeDisplay.tsx │ │ ├── BearingDisplay.tsx │ │ ├── ActiveArrow.tsx │ │ ├── DurationDisplay.tsx │ │ ├── RoundedDistanceDisplay.tsx │ │ ├── DistanceDisplay.tsx │ │ ├── SpeedDisplay.tsx │ │ ├── SuperDeviationBar.tsx │ │ ├── FuelDisplay.tsx │ │ └── FlightplanArrow.tsx │ ├── Blink.tsx │ ├── editors │ │ ├── RunwaySurfaceEditor.tsx │ │ ├── SpeedEditor.tsx │ │ ├── BearingEditor.tsx │ │ ├── FreetextEditor.tsx │ │ ├── DistanceEditor.tsx │ │ ├── NdbFreqEditor.tsx │ │ ├── ElevationEditor.tsx │ │ ├── TimeEditor.tsx │ │ ├── RadialEditor.tsx │ │ ├── VorFreqEditor.tsx │ │ ├── MagvarEditor.tsx │ │ └── DateEditor.tsx │ ├── WaypointDeleteListItem.tsx │ ├── ErrorPage.tsx │ └── Button.tsx ├── data │ ├── Constants.ts │ ├── Text.ts │ ├── navdata │ │ ├── IcaoFixedLength.ts │ │ ├── IcaoBuilder.ts │ │ ├── Database.ts │ │ ├── UniqueIdentGenerator.ts │ │ └── KLNMagvar.ts │ ├── Units.ts │ ├── Wind.ts │ ├── Conversions.ts │ └── MessageHandler.ts ├── services │ ├── SignalOutputFillterTick.ts │ ├── KeyboardService.ts │ ├── SignalOutputFilter.ts │ ├── Timers.ts │ ├── FlightplanUtils.ts │ ├── TemporaryWaypointDeleter.ts │ ├── HtAboveAirportAlert.ts │ ├── KLNNavmath.ts │ ├── AudioGenerator.ts │ ├── Flightplanloader.ts │ ├── AltAlert.ts │ └── MSA.ts ├── Hardware.ts ├── pages │ ├── left │ │ ├── Oth1Page.tsx │ │ ├── Sta4Page.tsx │ │ ├── Sta3Page.tsx │ │ ├── Set6Page.tsx │ │ ├── SelfTestLeftPage.tsx │ │ ├── Sta2Page.tsx │ │ ├── Set4Page.tsx │ │ ├── Set7Page.tsx │ │ ├── Tri0Page.tsx │ │ ├── Oth4Page.tsx │ │ ├── Set9Page.tsx │ │ ├── Set10Page.tsx │ │ ├── Oth10Page.tsx │ │ ├── Oth7Page.tsx │ │ ├── Set3Page.tsx │ │ ├── Set8Page.tsx │ │ ├── Oth8Page.tsx │ │ ├── Cal4Page.tsx │ │ ├── Cal1Page.tsx │ │ └── Cal5Page.tsx │ ├── OneSegmentPage.tsx │ ├── NullPage.tsx │ ├── ObsWarningPage.tsx │ ├── TakehomePage.tsx │ ├── VFROnlyPage.tsx │ ├── AiracPage.tsx │ └── right │ │ ├── Dt1Page.tsx │ │ ├── Dt2Page.tsx │ │ └── Dt3Page.tsx ├── settings │ ├── KLN90BUserSettingsSaverManager.ts │ ├── KLN90BUserWaypoints.ts │ ├── KLN90BUserRemarkSettings.ts │ ├── KLN90BUserFlightplans.ts │ ├── UserFlightplanLoaderV2.ts │ ├── UserFlightplanLoaderV1.ts │ ├── UserFlightplanPersistor.ts │ └── RemarksManager.ts ├── HEvents.ts └── LVars.ts ├── resources ├── html_ui │ └── Pages │ │ └── VCockpit │ │ └── Instruments │ │ └── NavSystems │ │ └── GPS │ │ └── KLN90B │ │ ├── Assets │ │ ├── gps_sbas.json │ │ ├── kln90b.ttf │ │ └── kln90b-map.ttf │ │ └── KLN90B.html ├── ContentInfo │ └── falcon71-kln90b │ │ └── Thumbnail.jpg ├── manifest.json └── layout.json ├── package.json ├── tsconfig.json ├── rollup.config.mjs └── cfg └── panel.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | .rollup.cache 4 | node_modules 5 | types -------------------------------------------------------------------------------- /kln90b/Version.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = '[VI]{version}[/VI]'; // 6 chars maximum! -------------------------------------------------------------------------------- /resources/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/gps_sbas.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /resources/ContentInfo/falcon71-kln90b/Thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falcon71/kln90b/HEAD/resources/ContentInfo/falcon71-kln90b/Thumbnail.jpg -------------------------------------------------------------------------------- /resources/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/kln90b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falcon71/kln90b/HEAD/resources/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/kln90b.ttf -------------------------------------------------------------------------------- /resources/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/kln90b-map.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falcon71/kln90b/HEAD/resources/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/kln90b-map.ttf -------------------------------------------------------------------------------- /kln90b/controls/Inverted.tsx: -------------------------------------------------------------------------------- 1 | import {DisplayComponent, FSComponent, VNode} from "@microsoft/msfs-sdk"; 2 | 3 | 4 | export class Inverted extends DisplayComponent { 5 | 6 | render(): VNode | null { 7 | return ({this.props.children}); 8 | } 9 | } -------------------------------------------------------------------------------- /kln90b/data/Constants.ts: -------------------------------------------------------------------------------- 1 | //How big one pixel is. Must be synchronized with KLN90B.scss $zoom-factor 2 | export const ZOOM_FACTOR = 4; 3 | 4 | export const CHAR_WIDTH = 9; 5 | export const CHAR_HEIGHT = 13; 6 | 7 | export const CHAR_WIDTH_MAP = 6; 8 | export const CHAR_HEIGHT_MAP = 7; 9 | 10 | 11 | export const MARGIN_X = 4; 12 | export const MARGIN_Y = 3; -------------------------------------------------------------------------------- /kln90b/services/SignalOutputFillterTick.ts: -------------------------------------------------------------------------------- 1 | import {CalcTickable} from "../TickController"; 2 | import {Sensors} from "../Sensors"; 3 | 4 | export class SignalOutputFillterTick implements CalcTickable { 5 | 6 | 7 | constructor(private readonly sensors: Sensors) { 8 | } 9 | 10 | public tick(): void { 11 | this.sensors.out.setFilteredOutputs(); 12 | 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /resources/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [], 3 | "content_type": "INSTRUMENTS", 4 | "title": "KLN 90B", 5 | "manufacturer": "", 6 | "creator": "falcon71", 7 | "package_version": "[VI]{version}[/VI]", 8 | "minimum_game_version": "1.30.12.0", 9 | "release_notes": { 10 | "neutral": { 11 | "LastUpdate": "", 12 | "OlderHistory": "" 13 | } 14 | }, 15 | "total_package_size": "00000000000006350497" 16 | } -------------------------------------------------------------------------------- /kln90b/data/Text.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The KLN can only display A TO Z and 0 to 9 uppercase. This function handles the conversion of arbitrary text. 3 | * The conversion currently simplay removes unknwon characters 4 | * @param text 5 | */ 6 | export function convertTextToKLNCharset(text: string): string { 7 | const upper = text.toUpperCase(); 8 | return upper.replace(/[^ A-Z0-9\-]/g, ""); //The regex matches everything except for spaces, minus, uppercase chars and digits 9 | } -------------------------------------------------------------------------------- /kln90b/data/navdata/IcaoFixedLength.ts: -------------------------------------------------------------------------------- 1 | import {Facility} from "@microsoft/msfs-sdk"; 2 | 3 | export class IcaoFixedLength { 4 | 5 | /** 6 | Returns the ident with a fixed length of 5 7 | * @param facility 8 | */ 9 | public static getIdentFromFacility(facility: Facility | null): string { 10 | if (facility === null) { 11 | return " "; 12 | } 13 | return facility.icaoStruct.ident.padEnd(5, " "); 14 | } 15 | } -------------------------------------------------------------------------------- /kln90b/data/Units.ts: -------------------------------------------------------------------------------- 1 | //Doesn't really do anything, but makes it clearer to the reader what unit is used 2 | 3 | export type Feet = number; 4 | export type Inhg = number; 5 | export type NauticalMiles = number; 6 | export type Latitude = number; 7 | export type Longitude = number; 8 | export type Degrees = number; 9 | export type Knots = number; 10 | export type Mph = number; 11 | export type Seconds = number; 12 | export type Celsius = number; 13 | export type Fahrenheit = number; 14 | export type Kelvin = number; -------------------------------------------------------------------------------- /kln90b/controls/selects/NdbSelector.tsx: -------------------------------------------------------------------------------- 1 | import {EventBus, FacilityClient, FacilitySearchType, NdbFacility} from "@microsoft/msfs-sdk"; 2 | import {WaypointSelector} from "./WaypointSelector"; 3 | 4 | 5 | export class NdbSelector extends WaypointSelector { 6 | 7 | constructor(bus: EventBus, ident: string, facilityLoader: FacilityClient, changedCallback: (icao: NdbFacility | string) => void) { 8 | super(bus, ident, facilityLoader, 3, FacilitySearchType.Ndb, changedCallback); 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /resources/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/KLN90B.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /kln90b/controls/selects/AirportSelector.tsx: -------------------------------------------------------------------------------- 1 | import {AirportFacility, EventBus, FacilityClient, FacilitySearchType} from "@microsoft/msfs-sdk"; 2 | import {WaypointSelector} from "./WaypointSelector"; 3 | 4 | 5 | export class AirportSelector extends WaypointSelector { 6 | 7 | constructor(bus: EventBus, ident: string, facilityLoader: FacilityClient, changedCallback: (icao: AirportFacility | string) => void) { 8 | super(bus, ident, facilityLoader, 4, FacilitySearchType.Airport, changedCallback); 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/SupplementarySelector.tsx: -------------------------------------------------------------------------------- 1 | import {EventBus, FacilityClient, FacilitySearchType, UserFacility} from "@microsoft/msfs-sdk"; 2 | import {WaypointSelector} from "./WaypointSelector"; 3 | 4 | 5 | export class SupplementarySelector extends WaypointSelector { 6 | 7 | constructor(bus: EventBus, ident: string, facilityLoader: FacilityClient, changedCallback: (icao: UserFacility | string) => void) { 8 | super(bus, ident, facilityLoader, 5, FacilitySearchType.User, changedCallback); 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/IntersectionSelector.tsx: -------------------------------------------------------------------------------- 1 | import {EventBus, FacilityClient, FacilitySearchType, IntersectionFacility} from "@microsoft/msfs-sdk"; 2 | import {WaypointSelector} from "./WaypointSelector"; 3 | 4 | 5 | export class IntersectionSelector extends WaypointSelector { 6 | 7 | constructor(bus: EventBus, ident: string, facilityLoader: FacilityClient, changedCallback: (icao: IntersectionFacility | string) => void) { 8 | super(bus, ident, facilityLoader, 5, FacilitySearchType.Intersection, changedCallback); 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /kln90b/Hardware.ts: -------------------------------------------------------------------------------- 1 | import {LVAR_RIGHT_SCAN} from "./LVars"; 2 | import {SimVarValueType} from "@microsoft/msfs-sdk"; 3 | 4 | export class Hardware { 5 | 6 | public isScanPulled: boolean = false; 7 | 8 | constructor() { 9 | SimVar.SetSimVarValue(LVAR_RIGHT_SCAN, SimVarValueType.Bool, this.isScanPulled); 10 | } 11 | 12 | public setScanPulled(scanPulled: boolean): void{ 13 | if(scanPulled !== this.isScanPulled){ 14 | this.isScanPulled = scanPulled; 15 | SimVar.SetSimVarValue(LVAR_RIGHT_SCAN, SimVarValueType.Bool, scanPulled); 16 | } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/VorSelector.tsx: -------------------------------------------------------------------------------- 1 | import {EventBus, FacilityClient, FacilitySearchType, VorFacility, VorType} from "@microsoft/msfs-sdk"; 2 | import {WaypointSelector} from "./WaypointSelector"; 3 | 4 | 5 | export class VorSelector extends WaypointSelector { 6 | 7 | constructor(bus: EventBus, ident: string, facilityLoader: FacilityClient, changedCallback: (icao: VorFacility | string) => void) { 8 | super(bus, ident, facilityLoader, 3, FacilitySearchType.Vor, changedCallback); 9 | } 10 | 11 | 12 | protected isValidResult(facility: VorFacility): boolean { 13 | return facility.type === VorType.VORDME || facility.type === VorType.DME || facility.type === VorType.VOR || facility.type === VorType.Unknown; 14 | } 15 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/TextDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | 5 | /** 6 | * Displays a simple dynamic text 7 | */ 8 | export class TextDisplay implements UiElement { 9 | readonly children = NO_CHILDREN; 10 | private ref: NodeReference = FSComponent.createRef(); 11 | 12 | constructor(public text: string) { 13 | } 14 | 15 | render(): VNode { 16 | return ({this.text}); 17 | } 18 | 19 | public tick(blnk: boolean): void { 20 | if (!TickController.checkRef(this.ref)) { 21 | return; 22 | } 23 | this.ref.instance.innerText = this.text; 24 | } 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kln90b", 3 | "//": "6 chars maximum for the version!", 4 | "version": "2.0.4", 5 | "description": "", 6 | "main": "index.ts", 7 | "scripts": { 8 | "build": "npx rollup -c" 9 | }, 10 | "keywords": [], 11 | "author": "falcon71", 12 | "license": "GPL-3.0-or-later", 13 | "devDependencies": { 14 | "@rollup/plugin-node-resolve": "^16.0.1", 15 | "@rollup/plugin-typescript": "^12.1.4", 16 | "@types/node": "^24.3.0", 17 | "rollup": "^4.41.1", 18 | "rollup-plugin-scss": "^4.0.1", 19 | "rollup-plugin-version-injector": "^1.3.3", 20 | "sass": "^1.90.0", 21 | "typescript": "^5.9.2", 22 | "tslib": "^2.5.0" 23 | }, 24 | "dependencies": { 25 | "@microsoft/msfs-sdk": "^2.2.3", 26 | "@microsoft/msfs-types": "^1.14.6", 27 | "numerable": "^0.3.15" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /kln90b/pages/left/Oth1Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {NO_CHILDREN, PageProps} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | 6 | 7 | /** 8 | * 3-52 9 | * TODO: I don't think MSFS has any FSS in it's DB. 10 | * The empty page can be seen here: https://www.youtube.com/shorts/9We5fcd2-VE 11 | */ 12 | export class Oth1Page extends SixLineHalfPage { 13 | 14 | public readonly cursorController = NO_CURSOR_CONTROLLER; 15 | readonly children = NO_CHILDREN; 16 | 17 | readonly name: string = "OTH 1"; 18 | 19 | constructor(props: PageProps) { 20 | super(props); 21 | } 22 | 23 | public render(): VNode { 24 | return (
25 |             NO NEAREST
26 | FSS 27 |
); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /kln90b/controls/Blink.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {TickController} from "../TickController"; 3 | import {NO_CHILDREN, UiElement} from "../pages/Page"; 4 | 5 | 6 | export class Blink implements UiElement { 7 | readonly children = NO_CHILDREN; 8 | private ref: NodeReference = FSComponent.createRef(); 9 | 10 | constructor(private contents: String) { 11 | } 12 | 13 | render(): VNode { 14 | return ({this.contents}); 15 | } 16 | 17 | public tick(blnk: boolean): void { 18 | if (!TickController.checkRef(this.ref)) { 19 | return; 20 | } 21 | if (blnk) { 22 | this.ref.instance.classList.add("blink"); 23 | } else { 24 | this.ref.instance.classList.remove("blink"); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": false, /* Enables incremental builds */ 4 | "target": "es2017", /* Specifies the ES2017 target, compatible with Coherent GT */ 5 | "module": "es2015", /* Ensures that modules are at least es2015 */ 6 | "strict": true, /* Enables strict type checking, highly recommended but optional */ 7 | "esModuleInterop": true, /* Emits additional JS to work with CommonJS modules */ 8 | "skipLibCheck": true, /* Skip type checking on library .d.ts files */ 9 | "forceConsistentCasingInFileNames": true, /* Ensures correct import casing */ 10 | "moduleResolution": "node", /* Enables compatibility with MSFS SDK bare global imports */ 11 | "jsxFactory": "FSComponent.buildComponent", /* Required for FSComponent framework JSX */ 12 | "jsxFragmentFactory": "FSComponent.Fragment", /* Required for FSComponent framework JSX */ 13 | "jsx": "react", /* Required for FSComponent framework JSX */ 14 | } 15 | } -------------------------------------------------------------------------------- /kln90b/settings/KLN90BUserSettingsSaverManager.ts: -------------------------------------------------------------------------------- 1 | import {EventBus, UserSettingSaveManager} from '@microsoft/msfs-sdk'; 2 | import {KLN90BUserSettings} from "./KLN90BUserSettings"; 3 | import {KLN90BUserWaypointsSettings} from "./KLN90BUserWaypoints"; 4 | import {KLN90BUserFlightplansSettings} from "./KLN90BUserFlightplans"; 5 | import {KLN90BUserRemarkSettings} from "./KLN90BUserRemarkSettings"; 6 | 7 | 8 | export class KLN90BSettingSaveManager extends UserSettingSaveManager { 9 | 10 | constructor(bus: EventBus, userSettings: KLN90BUserSettings) { 11 | const klnUserWaypoints = KLN90BUserWaypointsSettings.getManager(bus); 12 | const klnUserFlightplans = KLN90BUserFlightplansSettings.getManager(bus); 13 | const klnUserRemarks = KLN90BUserRemarkSettings.getManager(bus); 14 | const settings = userSettings.getAllSettings().concat(klnUserWaypoints.getAllSettings(), klnUserFlightplans.getAllSettings(), klnUserRemarks.getAllSettings()); 15 | 16 | super(settings, bus); 17 | } 18 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Sta4Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {NO_CHILDREN, PageProps} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | 6 | /** 7 | * 5-31 8 | */ 9 | export class Sta4Page extends SixLineHalfPage { 10 | 11 | public readonly cursorController = NO_CURSOR_CONTROLLER; 12 | readonly children = NO_CHILDREN; 13 | 14 | readonly name: string = "STA 4"; 15 | 16 | constructor(props: PageProps) { 17 | super(props); 18 | } 19 | 20 | public render(): VNode { 21 | //This page shows hours, I don't mind making it completely static 22 | return (
23 |             TOTAL TIME
24 | {Math.floor(this.props.userSettings.getSetting("totalTime").get() / 3600).toString().padStart(8, " ")} HR
25 | PWR CYCLES
26 | {this.props.userSettings.getSetting("powercycles").get().toString().padStart(8, " ")}
27 |
); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/TemperatureDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {Celsius} from "../../data/Units"; 5 | import {format} from "numerable"; 6 | 7 | 8 | export class TemperatureDisplay implements UiElement { 9 | readonly children = NO_CHILDREN; 10 | private readonly ref: NodeReference = FSComponent.createRef(); 11 | 12 | constructor(public temperature: Celsius) { 13 | } 14 | 15 | render(): VNode { 16 | return ({this.formatTemperature()}); 17 | } 18 | 19 | public tick(blnk: boolean): void { 20 | if (!TickController.checkRef(this.ref)) { 21 | return; 22 | } 23 | 24 | this.ref.instance.innerText = this.formatTemperature(); 25 | } 26 | 27 | private formatTemperature(): string { 28 | return `${format(this.temperature, "-00").padStart(3, " ")}°C`; 29 | } 30 | } -------------------------------------------------------------------------------- /kln90b/settings/KLN90BUserWaypoints.ts: -------------------------------------------------------------------------------- 1 | import {DefaultUserSettingManager, EventBus, UserSettingDefinition} from '@microsoft/msfs-sdk'; 2 | 3 | 4 | export type KLN90BUserWaypointsTypes = { 5 | [key: string]: string; 6 | }; 7 | 8 | export const MAX_USER_WAYPOINTS = 250; 9 | 10 | type Def = UserSettingDefinition; 11 | 12 | export class KLN90BUserWaypointsSettings { 13 | private static INSTANCE: DefaultUserSettingManager | undefined; 14 | 15 | public static getManager(bus: EventBus): DefaultUserSettingManager { 16 | return KLN90BUserWaypointsSettings.INSTANCE ??= new DefaultUserSettingManager(bus, this.buildSettingsDefs()); 17 | } 18 | 19 | private static buildSettingsDefs(): Def[] { 20 | const defs: Def[] = []; 21 | for (let i = 0; i < MAX_USER_WAYPOINTS; i++) { 22 | defs.push({ 23 | name: `wpt${i}`, 24 | defaultValue: "", 25 | }) 26 | } 27 | return defs; 28 | } 29 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/RunwaySurfaceEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, RunwaySurfaceType, VNode} from "@microsoft/msfs-sdk"; 3 | import {RunwaySurfaceEditorField} from "./EditorField"; 4 | 5 | export class RunwaySurfaceEditor extends Editor { 6 | 7 | constructor(bus: EventBus, value: number | null, enterCallback: (text: number) => void) { 8 | super(bus, [ 9 | new RunwaySurfaceEditorField(), 10 | ], value, enterCallback); 11 | } 12 | 13 | public render(): VNode { 14 | return ( 15 | {this.editorFields[0].render()} 16 | ); 17 | } 18 | 19 | protected convertFromValue(surface: RunwaySurfaceType): Rawvalue { 20 | return surface === RunwaySurfaceType.Asphalt ? [0] : [1]; 21 | } 22 | 23 | protected convertToValue(rawValue: Rawvalue): Promise { 24 | const newValue = rawValue[0] === 0 ? RunwaySurfaceType.Asphalt : RunwaySurfaceType.Grass; 25 | return Promise.resolve(newValue); 26 | } 27 | } -------------------------------------------------------------------------------- /kln90b/settings/KLN90BUserRemarkSettings.ts: -------------------------------------------------------------------------------- 1 | import {DefaultUserSettingManager, EventBus, UserSettingDefinition} from '@microsoft/msfs-sdk'; 2 | import {KLN90BUserWaypointsTypes} from "./KLN90BUserWaypoints"; 3 | 4 | 5 | export type KLN90BUserRemarkTypes = { 6 | [key: string]: string; 7 | }; 8 | 9 | export const NUM_REMARKS = 10; 10 | 11 | type Def = UserSettingDefinition; 12 | 13 | export class KLN90BUserRemarkSettings { 14 | private static INSTANCE: DefaultUserSettingManager | undefined; 15 | 16 | public static getManager(bus: EventBus): DefaultUserSettingManager { 17 | return KLN90BUserRemarkSettings.INSTANCE ??= new DefaultUserSettingManager(bus, this.buildSettingsDefs()); 18 | } 19 | 20 | private static buildSettingsDefs(): Def[] { 21 | const defs: Def[] = []; 22 | for (let i = 0; i < NUM_REMARKS; i++) { 23 | defs.push({ 24 | name: `rmk${i}`, 25 | defaultValue: "", 26 | }) 27 | } 28 | return defs; 29 | } 30 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Sta3Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {NO_CHILDREN, PageProps} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {VERSION} from "../../Version"; 6 | 7 | 8 | /** 9 | * 5-31 10 | */ 11 | export class Sta3Page extends SixLineHalfPage { 12 | 13 | public readonly cursorController = NO_CURSOR_CONTROLLER; 14 | readonly children = NO_CHILDREN; 15 | 16 | readonly name: string = "STA 3"; 17 | 18 | constructor(props: PageProps) { 19 | super(props); 20 | } 21 | 22 | public render(): VNode { 23 | //From the installation manual 2.4.1: CAL 0 -10°, CAL 100: 0° (default), CAL 200: +10° 24 | //can be adjusted by holding the left CRS Button when turning the device on 25 | return (
26 |             HOST SW
27 | {VERSION.padStart(11, " ")}
28 | RCVR SW
29 |          02
30 |
31 | OBS CAL 100 32 |
); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /kln90b/settings/KLN90BUserFlightplans.ts: -------------------------------------------------------------------------------- 1 | import {DefaultUserSettingManager, EventBus, UserSettingDefinition} from '@microsoft/msfs-sdk'; 2 | import {KLN90BUserWaypointsTypes} from "./KLN90BUserWaypoints"; 3 | 4 | 5 | export type KLN90BUserFlightplansTypes = { 6 | [key: string]: string; 7 | }; 8 | 9 | export const NUM_FLIGHTPLANS = 25; 10 | 11 | type Def = UserSettingDefinition; 12 | 13 | export class KLN90BUserFlightplansSettings { 14 | private static INSTANCE: DefaultUserSettingManager | undefined; 15 | 16 | public static getManager(bus: EventBus): DefaultUserSettingManager { 17 | return KLN90BUserFlightplansSettings.INSTANCE ??= new DefaultUserSettingManager(bus, this.buildSettingsDefs()); 18 | } 19 | 20 | private static buildSettingsDefs(): Def[] { 21 | const defs: Def[] = []; 22 | for (let i = 0; i <= NUM_FLIGHTPLANS; i++) { 23 | defs.push({ 24 | name: `fpl${i}`, 25 | defaultValue: "", 26 | }) 27 | } 28 | return defs; 29 | } 30 | } -------------------------------------------------------------------------------- /kln90b/pages/OneSegmentPage.tsx: -------------------------------------------------------------------------------- 1 | import {DisplayComponent, FSComponent, VNode} from "@microsoft/msfs-sdk"; 2 | import {CursorController} from "./CursorController"; 3 | import {PageProps, UiElement, UIElementChildren} from "./Page"; 4 | 5 | export abstract class SevenLinePage extends DisplayComponent implements UiElement { 6 | abstract readonly lCursorController: CursorController; 7 | abstract readonly rCursorController: CursorController; 8 | abstract children: UIElementChildren; 9 | 10 | abstract render(): VNode 11 | 12 | enter(): boolean { 13 | return false; 14 | } 15 | 16 | isEnterAccepted(): boolean { 17 | return this.lCursorController.isEnterAccepted() || this.rCursorController.isEnterAccepted(); 18 | } 19 | 20 | msg(): boolean { 21 | return false; 22 | } 23 | 24 | tick(blink: boolean): void { 25 | } 26 | 27 | public scanRight(): boolean { 28 | return false; 29 | } 30 | 31 | public scanLeft(): boolean { 32 | return false; 33 | } 34 | 35 | 36 | clear(): boolean { 37 | return this.lCursorController.clear() || this.rCursorController.clear(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /kln90b/pages/NullPage.tsx: -------------------------------------------------------------------------------- 1 | import {DisplayComponent, FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {NO_CHILDREN, Page} from "./Page"; 3 | 4 | /** 5 | * Empty Page, when the device is turned off 6 | */ 7 | export class NullPage extends DisplayComponent implements Page { 8 | 9 | readonly children = NO_CHILDREN; 10 | 11 | public render(): VNode { 12 | return ( 13 |

14 |         );
15 |     }
16 | 
17 |     public isEnterAccepted(): boolean {
18 |         return false;
19 |     }
20 | 
21 |     public onInteractionEvent(evt: string): boolean {
22 |         return true;
23 |     }
24 | 
25 |     public tick(blink: boolean): void {
26 |     }
27 | 
28 |     public isLeftCursorActive(): boolean {
29 |         return false;
30 |     }
31 | 
32 |     public isRightCursorActive(): boolean {
33 |         return false;
34 |     }
35 | 
36 | 
37 |     public leftPageName(): string {
38 |         return "     ";
39 |     }
40 | 
41 |     public rightPageName(): string {
42 |         return "     ";
43 |     }
44 | 
45 |     public isMessagePageShown(): boolean {
46 |         return false;
47 |     }
48 | 
49 |     public hasStatusline(): boolean {
50 |         return false;
51 |     }
52 | }


--------------------------------------------------------------------------------
/kln90b/services/KeyboardService.ts:
--------------------------------------------------------------------------------
 1 | import {EVT_KEY} from "../HEvents";
 2 | import {CursorController} from "../pages/CursorController";
 3 | 
 4 | export class KeyboardService {
 5 | 
 6 |     public static routeKeyboardEvent(evt: string, lCursorController: CursorController, rCursorController: CursorController): boolean {
 7 |         if (!evt.startsWith(EVT_KEY)) {
 8 |             return false;
 9 |         }
10 | 
11 |         const split = evt.split(':');
12 |         const key = split[2];
13 |         let handled;
14 |         switch (split[1]) {
15 |             case 'LEFT':
16 |                 handled = lCursorController.keyboard(key);
17 |                 if (handled) {
18 |                     lCursorController.outerRight(); //Automatically advance to the next field
19 |                 }
20 |                 return handled;
21 |             case 'RIGHT':
22 |                 handled = rCursorController.keyboard(key);
23 |                 if (handled) {
24 |                     rCursorController.outerRight(); //Automatically advance to the next field
25 |                 }
26 |                 return handled;
27 |             default:
28 |                 throw new Error(`Unexpected page: ${evt}`);
29 | 
30 |         }
31 | 
32 | 
33 |     }
34 | 
35 | }


--------------------------------------------------------------------------------
/kln90b/pages/ObsWarningPage.tsx:
--------------------------------------------------------------------------------
 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk';
 2 | import {NO_CURSOR_CONTROLLER} from "./CursorController";
 3 | import {FourSegmentPage, SixLinePage} from "./FourSegmentPage";
 4 | import {NO_CHILDREN, PageProps} from "./Page";
 5 | import {AiracPage} from "./AiracPage";
 6 | 
 7 | 
 8 | export class ObswarningPage extends SixLinePage {
 9 | 
10 |     readonly children = NO_CHILDREN;
11 |     public readonly lCursorController = NO_CURSOR_CONTROLLER;
12 |     public readonly rCursorController = NO_CURSOR_CONTROLLER;
13 | 
14 |     constructor(props: PageProps) {
15 |         super(props);
16 |     }
17 | 
18 |     public render(): VNode {
19 |         return (
20 |             
21 |         WARNING
22 |
23 |  SYSTEM IS IN OBS MODE
24 |  PRESS GPS CRS BUTTON
25 |  TO CHANGE TO LEG MODE 26 |
); 27 | } 28 | 29 | tick(blink: boolean) { 30 | super.tick(blink); 31 | if (!this.props.modeController.isObsModeActive()) { 32 | this.props.pageManager.setCurrentPage(FourSegmentPage, { 33 | ...this.props, 34 | page: new AiracPage(this.props), 35 | }); 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /kln90b/data/navdata/IcaoBuilder.ts: -------------------------------------------------------------------------------- 1 | import {ICAO, IcaoValue} from "@microsoft/msfs-sdk"; 2 | 3 | export const TEMPORARY_WAYPOINT = 'XY'; 4 | export const USER_WAYPOINT = 'XX'; 5 | 6 | /** 7 | * @deprecated use buildIcaoStruct 8 | * @param type 9 | * @param region 10 | * @param ident 11 | */ 12 | export function buildIcao(type: 'A' | 'W' | 'V' | 'N' | 'U', region: string, ident: string): string { 13 | return buildIcaoWithAirport(type, region, ' ', ident); 14 | } 15 | 16 | /** 17 | * @deprecated use buildIcaoStructWithAirport 18 | * @param type 19 | * @param region 20 | * @param airport 21 | * @param ident 22 | */ 23 | export function buildIcaoWithAirport(type: 'A' | 'W' | 'V' | 'N' | 'U', region: string, airport: string, ident: string): string { 24 | return type + region + airport + ident.padEnd(5, " "); 25 | } 26 | 27 | export function buildIcaoStruct(type: 'A' | 'W' | 'V' | 'N' | 'U', region: string, ident: string): IcaoValue { 28 | return buildIcaoStructWithAirport(type, region, '', ident); 29 | } 30 | 31 | export function buildIcaoStructIdentOnly(ident: string): IcaoValue { 32 | return buildIcaoStructWithAirport('', '', '', ident); 33 | } 34 | 35 | export function buildIcaoStructWithAirport(type: 'A' | 'W' | 'V' | 'N' | 'U' | '', region: string, airport: string, ident: string): IcaoValue { 36 | return ICAO.value(type, region, airport, ident); 37 | } 38 | -------------------------------------------------------------------------------- /kln90b/data/Wind.ts: -------------------------------------------------------------------------------- 1 | import {Degrees, Knots} from "./Units"; 2 | import {NavMath} from "@microsoft/msfs-sdk"; 3 | 4 | /** 5 | * https://edwilliams.org/avform147.htm#Wind 6 | * @param tas 7 | * @param windspeed 8 | * @param windDirection 9 | * @param heading 10 | */ 11 | export function calculateGroundspeed(tas: Knots, heading: Degrees, windspeed: Knots, windDirection: Degrees) { 12 | return Math.sqrt((windspeed ** 2) + (tas ** 2) - 2 * windspeed * tas * Math.cos(heading * Avionics.Utils.DEG2RAD - windDirection * Avionics.Utils.DEG2RAD)); 13 | } 14 | 15 | /** 16 | * https://edwilliams.org/avform147.htm#Wind 17 | */ 18 | export function calculateWindspeed(tas: Knots, gs: Knots, heading: Degrees, track: Degrees): Knots { 19 | return Math.sqrt((tas - gs) ** 2 + 4 * tas * gs * (Math.sin(((heading - track) * Avionics.Utils.DEG2RAD) / 2)) ** 2); 20 | } 21 | 22 | export function calculateWindDirection(tas: Knots, gs: Knots, heading: Degrees, track: Degrees): Degrees { 23 | return NavMath.normalizeHeading((track * Avionics.Utils.DEG2RAD + Math.atan2(tas * Math.sin((heading - track) * Avionics.Utils.DEG2RAD), tas * Math.cos((heading - track) * Avionics.Utils.DEG2RAD) - gs)) * Avionics.Utils.RAD2DEG); 24 | } 25 | 26 | export function calculateHeadwind(windSpeed: Knots, windDirection: Degrees, heading: Degrees): Knots { 27 | return windSpeed * Math.cos((windDirection - heading) * Avionics.Utils.DEG2RAD); 28 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/SpeedEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {NumberEditorField} from "./EditorField"; 4 | import {format} from "numerable"; 5 | 6 | export class SpeedEditor extends Editor { 7 | 8 | constructor(bus: EventBus, value: number | null, enterCallback: (text: number) => void) { 9 | super(bus, [ 10 | NumberEditorField.createWithBlankMax(9), 11 | new NumberEditorField(), 12 | new NumberEditorField(), 13 | ], value, enterCallback); 14 | } 15 | 16 | public render(): VNode { 17 | return ( 18 | {this.editorFields[0].render()}{this.editorFields[1].render()}{this.editorFields[2].render()} 19 | ); 20 | } 21 | 22 | protected convertFromValue(radial: number): Rawvalue { 23 | const numberString = format(radial, "000"); 24 | return [ 25 | Number(numberString.substring(0, 1)), 26 | Number(numberString.substring(1, 2)), 27 | Number(numberString.substring(2, 3)), 28 | ]; 29 | } 30 | 31 | protected convertToValue(rawValue: Rawvalue): Promise { 32 | const newValue = Number(String(rawValue[0]) + String(rawValue[1]) + String(rawValue[2])); 33 | return Promise.resolve(newValue); 34 | } 35 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/TimeDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {format} from "numerable"; 5 | import {TimeStamp} from "../../data/Time"; 6 | 7 | /** 8 | * Displays a time in the format HH:MM 9 | */ 10 | export class TimeDisplay implements UiElement { 11 | readonly children = NO_CHILDREN; 12 | public isVisible = true; 13 | private readonly ref: NodeReference = FSComponent.createRef(); 14 | 15 | constructor(public time: TimeStamp | null = null) { 16 | } 17 | 18 | render(): VNode { 19 | return ({this.formatTime()}); 20 | } 21 | 22 | public tick(blnk: boolean): void { 23 | if (!TickController.checkRef(this.ref)) { 24 | return; 25 | } 26 | if (this.isVisible) { 27 | this.ref!.instance.classList.remove("d-none"); 28 | } else { 29 | this.ref!.instance.classList.add("d-none"); 30 | } 31 | this.ref.instance.innerText = this.formatTime(); 32 | } 33 | 34 | private formatTime(): string { 35 | if (this.time === null) { 36 | return "--:--"; 37 | } 38 | 39 | return `${format(this.time.getHours(), "00")}:${format(this.time.getMinutes(), "00")}`; 40 | } 41 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/AltitudeDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {Feet} from "../../data/Units"; 5 | 6 | /** 7 | * Displays a formatted Altiude 8 | */ 9 | export class AltitudeDisplay implements UiElement { 10 | readonly children = NO_CHILDREN; 11 | private readonly ref: NodeReference = FSComponent.createRef(); 12 | 13 | protected readonly rounding: number = 100; 14 | 15 | constructor(public altitude: Feet | null = null) { 16 | } 17 | 18 | render(): VNode { 19 | return ({this.formatAltitude()}); 20 | } 21 | 22 | public tick(blink: boolean): void { 23 | if (!TickController.checkRef(this.ref)) { 24 | return; 25 | } 26 | this.ref.instance.innerText = this.formatAltitude(); 27 | } 28 | 29 | private formatAltitude(): string { 30 | if (this.altitude === null) { 31 | return "-----"; 32 | } 33 | 34 | const rounded = Utils.Clamp(Math.round(this.altitude / this.rounding) * this.rounding, 0, 65600); //https://youtu.be/gjmVrkHTdP0?t=27 35 | return rounded.toString().padStart(5, " "); 36 | } 37 | } 38 | 39 | export class ElevationDisplay extends AltitudeDisplay { 40 | protected readonly rounding: number = 10; 41 | } -------------------------------------------------------------------------------- /kln90b/data/navdata/Database.ts: -------------------------------------------------------------------------------- 1 | import {TimeStamp} from "../Time"; 2 | import {AiracCycleFormatter, EventBus, FacilityLoader} from "@microsoft/msfs-sdk"; 3 | import {Sensors} from "../../Sensors"; 4 | import {MessageHandler, OneTimeMessage} from "../MessageHandler"; 5 | import {GPSEvents} from "../../Gps"; 6 | 7 | 8 | export class Database { 9 | 10 | 11 | public readonly expirationDateString: string; 12 | private readonly expirationTimestamp: TimeStamp; 13 | 14 | constructor(bus: EventBus, private sensors: Sensors, private messageHandler: MessageHandler) { 15 | const airac = FacilityLoader.getDatabaseCycles().current; 16 | 17 | this.expirationTimestamp = TimeStamp.create(airac.expirationTimestamp); 18 | this.expirationDateString = AiracCycleFormatter.create('{expMinus({dd} {MON} {YYYY})}')(airac); 19 | 20 | console.log("airac", airac, this.expirationTimestamp); 21 | bus.getSubscriber().on("timeUpdatedEvent").handle(this.onGPSAcquired.bind(this)); 22 | } 23 | 24 | public isAiracCurrent(time: TimeStamp = this.sensors.in.gps.timeZulu): boolean { 25 | return time.getTimestamp() <= this.expirationTimestamp.getTimestamp(); 26 | } 27 | 28 | private onGPSAcquired(time: TimeStamp) { 29 | if (!this.isAiracCurrent(time)) { 30 | this.messageHandler.addMessage(new OneTimeMessage(["DATA BASE OUT OF DATE", "ALL DATA MUST BE", "CONFIRMED BEFORE USE"])) 31 | } 32 | } 33 | 34 | 35 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/BearingEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {NumberEditorField} from "./EditorField"; 4 | import {format} from "numerable"; 5 | 6 | export class BearingEditor extends Editor { 7 | 8 | constructor(bus: EventBus, value: number | null, enterCallback: (text: number) => void) { 9 | super(bus, [ 10 | NumberEditorField.createWithMinMax(0, 3), 11 | new NumberEditorField(), 12 | new NumberEditorField(), 13 | ], value, enterCallback); 14 | } 15 | 16 | public render(): VNode { 17 | return ( 18 | {this.editorFields[0].render()}{this.editorFields[1].render()}{this.editorFields[2].render()} 19 | ); 20 | } 21 | 22 | protected convertFromValue(radial: number): Rawvalue { 23 | const numberString = format(radial, "000"); 24 | return [ 25 | Number(numberString.substring(0, 1)), 26 | Number(numberString.substring(1, 2)), 27 | Number(numberString.substring(2, 3)), 28 | ]; 29 | } 30 | 31 | protected convertToValue(rawValue: Rawvalue): Promise { 32 | const newValue = Number(String(rawValue[0]) + String(rawValue[1]) + String(rawValue[2])); 33 | if (newValue >= 360) { 34 | return Promise.resolve(null); 35 | } 36 | 37 | return Promise.resolve(newValue); 38 | } 39 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/LatitudeDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {Latitude} from "../../data/Units"; 5 | import {format} from "numerable"; 6 | 7 | /** 8 | * Displays a formatted bearing/course/radial 9 | */ 10 | export class LatitudeDisplay implements UiElement { 11 | readonly children = NO_CHILDREN; 12 | private ref: NodeReference = FSComponent.createRef(); 13 | 14 | constructor(public latitude: Latitude | null = null) { 15 | } 16 | 17 | render(): VNode { 18 | return ({this.formatLatitude()}); 19 | } 20 | 21 | public tick(blink: boolean): void { 22 | if (!TickController.checkRef(this.ref)) { 23 | return; 24 | } 25 | this.ref.instance.innerText = this.formatLatitude(); 26 | } 27 | 28 | private formatLatitude(): string { 29 | if (this.latitude === null) { 30 | return "- --°--.--'"; 31 | } 32 | 33 | const northSount = this.latitude > 0 ? "N" : "S"; 34 | const latitude = Math.abs(this.latitude); 35 | const degreesString = format(latitude, "00", {rounding: "floor"}); 36 | 37 | const minutes = (latitude % 1) * 60; 38 | const minutesString = format(minutes, "00.00"); 39 | 40 | return `${northSount} ${degreesString}°${minutesString}'`; 41 | } 42 | } -------------------------------------------------------------------------------- /kln90b/services/SignalOutputFilter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The real KLN has analog discrete outputs for deviation bar signals. Based on page 186, it appears that the signal 3 | * is updated at 1HZ, but some circuitry filters and smoothes this signal. We try to emulate this behaviour here to 4 | * achive a smooth output for the GPS WP CROSS TRK SimVar 5 | */ 6 | export class SignalOutputFilter { 7 | 8 | lastPredictedValue = 0; 9 | vPredicted = 0; 10 | lastMeasuredValue = 0; 11 | lastMeasurementTime = new Date(0); 12 | 13 | setValue(measuredValue: number) { 14 | const now = new Date(); 15 | const tDiff = now.getTime() - this.lastMeasurementTime.getTime(); 16 | 17 | this.lastPredictedValue += this.vPredicted * tDiff; 18 | 19 | const vMeasured = (measuredValue - this.lastMeasuredValue) / tDiff; 20 | const nextPredictedValue = measuredValue + vMeasured * tDiff; 21 | 22 | //We don't want to jump to the measuredValue right away, so let's calculate a speed from 23 | // lastPredictedValue to nextPredictedValue for a smooth animation 24 | this.vPredicted = (nextPredictedValue - this.lastPredictedValue) / tDiff; 25 | 26 | this.lastMeasuredValue = measuredValue; 27 | this.lastMeasurementTime = now; 28 | } 29 | 30 | getCurrentValue() { 31 | const now = new Date(); 32 | const tDiff = now.getTime() - this.lastMeasurementTime.getTime(); 33 | 34 | return this.lastPredictedValue + this.vPredicted * tDiff; 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/FreetextEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {AlphabetEditorField} from "./EditorField"; 4 | 5 | export class FreetextEditor extends Editor { 6 | 7 | constructor(bus: EventBus, value: string, maxLength: number, enterCallback: (text: string) => void) { 8 | super(bus, Array(maxLength).fill(null).map(() => new AlphabetEditorField()), value, enterCallback); 9 | } 10 | 11 | public render(): VNode { 12 | return ({this.editorFields.map(f => f.render())}); 13 | } 14 | 15 | protected convertFromValue(value: string): Rawvalue { 16 | let strIdx = 0; 17 | const numberVal = Array(this.editorFields.length); 18 | for (let i = 0; i < this.editorFields.length; i++) { 19 | const field = this.editorFields[i]; 20 | const fieldLength = field.charset[0].length; 21 | const fieldVal = value.substring(strIdx, strIdx + fieldLength); 22 | numberVal[i] = field.charset.indexOf(fieldVal); 23 | strIdx += fieldLength; 24 | } 25 | return numberVal; 26 | } 27 | 28 | 29 | protected convertToValue(rawValue: Rawvalue): Promise { 30 | const val = Array(rawValue.length); 31 | for (let i = 0; i < rawValue.length; i++) { 32 | val[i] = this.editorFields[i].charset[rawValue[i]]; 33 | } 34 | 35 | return Promise.resolve(val.join("")); 36 | } 37 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Set6Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {SelectField} from "../../controls/selects/SelectField"; 6 | 7 | 8 | type Set6PageTypes = { 9 | enable: SelectField, 10 | } 11 | 12 | /** 13 | * 4-9 14 | */ 15 | export class Set6Page extends SixLineHalfPage { 16 | 17 | public readonly cursorController; 18 | readonly children: UIElementChildren; 19 | 20 | readonly name: string = "SET 6"; 21 | 22 | 23 | constructor(props: PageProps) { 24 | super(props); 25 | 26 | 27 | const enabled = this.props.userSettings.getSetting("turnAnticipation").get(); 28 | 29 | 30 | this.children = new UIElementChildren({ 31 | enable: new SelectField(["DISABLE", " ENABLE"], enabled ? 1 : 0, this.saveEnableTurnanticipation.bind(this)), 32 | }); 33 | 34 | this.cursorController = new CursorController(this.children); 35 | } 36 | 37 | public render(): VNode { 38 | return (
39 |                TURN
40 | ANTICIPATE
41 |
42 |  {this.children.get("enable").render()} 43 |
); 44 | } 45 | 46 | private saveEnableTurnanticipation(enabled: number): void { 47 | this.props.userSettings.getSetting("turnAnticipation").set(enabled === 1); 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/LongitudeDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {Longitude} from "../../data/Units"; 5 | import {format} from "numerable"; 6 | 7 | 8 | /** 9 | * Displays a formatted bearing/course/radial 10 | */ 11 | export class LongitudeDisplay implements UiElement { 12 | readonly children = NO_CHILDREN; 13 | private ref: NodeReference = FSComponent.createRef(); 14 | 15 | constructor(public longitude: Longitude | null = null) { 16 | } 17 | 18 | render(): VNode { 19 | return ({this.formatLongitude()}); 20 | } 21 | 22 | public tick(blink: boolean): void { 23 | if (!TickController.checkRef(this.ref)) { 24 | return; 25 | } 26 | this.ref.instance.innerText = this.formatLongitude(); 27 | } 28 | 29 | private formatLongitude(): string { 30 | if (this.longitude === null) { 31 | return "- --°--.--'"; 32 | } 33 | 34 | const eastWest = this.longitude > 0 ? "E" : "W"; 35 | const longitude = Math.abs(this.longitude); 36 | const degreesString = format(longitude, "00", {rounding: "floor"}).padStart(3, " "); 37 | 38 | const minutes = (longitude % 1) * 60; 39 | const minutesString = format(minutes, "00.00"); 40 | 41 | return `${eastWest}${degreesString}°${minutesString}'`; 42 | } 43 | } -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import scss from "rollup-plugin-scss"; 4 | import versionInjector from "rollup-plugin-version-injector"; 5 | import fs from "fs"; 6 | 7 | const buildTargetDir = process.env.buildTargetDir ?? 'build'; 8 | 9 | /** 10 | * Copies all resources to the build directory and inserts the version number from the package.json into the manifest 11 | * @param userConfig 12 | * @returns {{generateBundle(*, *): void}} 13 | */ 14 | function copyResourcesAndUpdateManifest(userConfig) { 15 | return { 16 | generateBundle(outputOptions, bundle) { 17 | const packageFile = JSON.parse(fs.readFileSync('package.json', 'utf8')); 18 | const version = packageFile.version; 19 | 20 | fs.cpSync('resources', userConfig.outputPath, {recursive: true}); 21 | let content = fs.readFileSync('resources/manifest.json', 'utf8'); 22 | content = content.replace('[VI]{version}[/VI]', version); 23 | fs.writeFileSync(`${userConfig.outputPath}/manifest.json`, content); 24 | } 25 | } 26 | } 27 | 28 | export default { 29 | input: 'kln90b/KLN90B.tsx', 30 | output: { 31 | sourcemap: true, 32 | dir: `${buildTargetDir}/html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B`, 33 | format: 'es' 34 | }, 35 | plugins: [ 36 | scss({ fileName: 'KLN90B.css' }), 37 | resolve(), 38 | copyResourcesAndUpdateManifest({ 39 | outputPath: buildTargetDir, 40 | }), 41 | versionInjector({ 42 | injectInComments: false, 43 | }), 44 | typescript(), 45 | ] 46 | } -------------------------------------------------------------------------------- /kln90b/HEvents.ts: -------------------------------------------------------------------------------- 1 | //POWER 2 | export const EVT_BRT_INC = "KLN90B_Brt_Inc"; 3 | export const EVT_BRT_DEC = "KLN90B_Brt_Dec"; 4 | export const EVT_POWER = "KLN90B_Power_Toggle"; 5 | export const EVT_POWER_ON = "KLN90B_Power_On"; //Intended for hardware 6 | export const EVT_POWER_OFF = "KLN90B_Power_Off"; //Intended for hardware 7 | 8 | //MENU 9 | export const EVT_MSG = "KLN90B_MSG_Push"; 10 | export const EVT_DCT = "KLN90B_DCT_Push"; 11 | export const EVT_CLR = "KLN90B_CLR_Push"; 12 | export const EVT_ENT = "KLN90B_ENT_Push"; 13 | export const EVT_ALT = "KLN90B_ALT_Push"; 14 | 15 | //L KNOB 16 | 17 | export const EVT_L_CURSOR = "KLN90B_LeftCursor_Toggle"; 18 | export const EVT_L_OUTER_LEFT = "KLN90B_LeftLargeKnob_Left"; 19 | export const EVT_L_OUTER_RIGHT = "KLN90B_LeftLargeKnob_Right"; 20 | export const EVT_L_INNER_LEFT = "KLN90B_LeftSmallKnob_Left"; 21 | export const EVT_L_INNER_RIGHT = "KLN90B_LeftSmallKnob_Right"; 22 | 23 | //R KNOB 24 | 25 | export const EVT_R_CURSOR = "KLN90B_RightCursor_Toggle"; 26 | export const EVT_R_OUTER_LEFT = "KLN90B_RightLargeKnob_Left"; 27 | export const EVT_R_OUTER_RIGHT = "KLN90B_RightLargeKnob_Right"; 28 | export const EVT_R_INNER_LEFT = "KLN90B_RightSmallKnob_Left"; 29 | export const EVT_R_INNER_RIGHT = "KLN90B_RightSmallKnob_Right"; 30 | export const EVT_R_SCAN = "KLN90B_RightScan_Toggle"; 31 | 32 | 33 | 34 | //External Devices 35 | export const EVT_APPR_ARM = "KLN90B_ApprArm_Push"; 36 | 37 | //Internal, do not use 38 | export const EVT_R_SCAN_LEFT = "KLN90B_Internal_RightScan_Left"; 39 | export const EVT_R_SCAN_RIGHT = "KLN90B_Internal_RightScan_Right"; 40 | export const EVT_KEY = "KLN90B_Internal_Key:"; -------------------------------------------------------------------------------- /kln90b/controls/displays/BearingDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {Degrees} from "../../data/Units"; 5 | import {format} from "numerable"; 6 | 7 | /** 8 | * Displays a formatted bearing/course/radial 9 | */ 10 | export class BearingDisplay implements UiElement { 11 | readonly children = NO_CHILDREN; 12 | public flash: boolean = false; 13 | public isVisible = true; 14 | private ref: NodeReference = FSComponent.createRef(); 15 | 16 | constructor(public bearing: Degrees | null = null) { 17 | } 18 | 19 | render(): VNode { 20 | return ({this.formatBearing()}); 21 | } 22 | 23 | public tick(blink: boolean): void { 24 | if (!TickController.checkRef(this.ref)) { 25 | return; 26 | } 27 | 28 | if (this.isVisible) { 29 | this.ref!.instance.classList.remove("d-none"); 30 | } else { 31 | this.ref!.instance.classList.add("d-none"); 32 | } 33 | 34 | this.ref.instance.innerText = this.formatBearing(); 35 | if (this.flash && blink) { 36 | this.ref.instance.classList.add("blink"); 37 | } else { 38 | this.ref.instance.classList.remove("blink"); 39 | } 40 | } 41 | 42 | private formatBearing(): string { 43 | if (this.bearing === null) { 44 | return "---°"; 45 | } 46 | return `${format(this.bearing, "000")}°`; 47 | } 48 | } -------------------------------------------------------------------------------- /kln90b/pages/left/SelfTestLeftPage.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {DeviationBar} from "../../controls/displays/DeviationBar"; 6 | import {BearingDisplay} from "../../controls/displays/BearingDisplay"; 7 | 8 | 9 | type SelfTestLeftPageTypes = { 10 | scale: DeviationBar, 11 | obsIn: BearingDisplay, 12 | } 13 | 14 | export class SelfTestLeftPage extends SixLineHalfPage { 15 | 16 | public readonly cursorController = NO_CURSOR_CONTROLLER; 17 | readonly children = new UIElementChildren({ 18 | scale: new DeviationBar(-0.5, false, 1), 19 | obsIn: new BearingDisplay(this.props.sensors.in.obsMag), 20 | }); 21 | 22 | readonly name: string = " "; 23 | 24 | constructor(props: PageProps) { 25 | super(props); 26 | 27 | this.props.memory.navPage.isSelfTestActive = true; 28 | } 29 | 30 | 31 | public render(): VNode { 32 | return (
33 |             DIS  34.5NM
34 | {this.children.get("scale").render()}
35 | OBS IN {this.children.get("obsIn").render()}
36 |    OUT 315°
37 | RMI 130°
38 | ANNUN ON 39 |
); 40 | } 41 | 42 | tick(blink: boolean) { 43 | this.requiresRedraw = true; 44 | super.tick(blink); 45 | } 46 | 47 | protected redraw() { 48 | this.children.get("obsIn").bearing = this.props.sensors.in.obsMag; 49 | } 50 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Sta2Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {UIElementChildren} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {TextDisplay} from "../../controls/displays/TextDisplay"; 6 | import {format} from "numerable"; 7 | 8 | 9 | type Sta2PageTypes = { 10 | posError: TextDisplay; 11 | } 12 | 13 | /** 14 | * 5-30 15 | */ 16 | export class Sta2Page extends SixLineHalfPage { 17 | 18 | public readonly cursorController = NO_CURSOR_CONTROLLER; 19 | readonly children: UIElementChildren = new UIElementChildren({ 20 | posError: new TextDisplay(".--"), 21 | }); 22 | 23 | readonly name: string = "STA 2"; 24 | 25 | protected readonly ref: NodeReference = FSComponent.createRef(); 26 | 27 | 28 | public render(): VNode { 29 | return (
30 |             ESTIMATED
31 | POSN ERROR
32 |       {this.children.get("posError").render()}nm 33 |
); 34 | } 35 | 36 | public tick(blink: boolean): void { 37 | this.requiresRedraw = true; 38 | super.tick(blink); 39 | } 40 | 41 | protected redraw(): void { 42 | if (this.props.sensors.in.gps.isValid()) { 43 | const posError = this.props.sensors.in.gps.gpsSatComputer.hdop * 4; 44 | this.children.get("posError").text = `.${format(posError, "00")}`; 45 | } else { 46 | this.children.get("posError").text = ".--"; 47 | } 48 | 49 | } 50 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/DistanceEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {NumberEditorField} from "./EditorField"; 4 | import {format} from "numerable"; 5 | 6 | export class DistanceEditor extends Editor { 7 | 8 | constructor(bus: EventBus, value: number | null, enterCallback: (text: number) => void) { 9 | super(bus, [ 10 | NumberEditorField.createWithMinMax(0, 3), 11 | new NumberEditorField(), 12 | new NumberEditorField(), 13 | new NumberEditorField(), 14 | ], value, enterCallback); 15 | } 16 | 17 | public render(): VNode { 18 | return ( 19 | {this.editorFields[0].render()}{this.editorFields[1].render()}{this.editorFields[2].render()}.{this.editorFields[3].render()} 20 | ); 21 | } 22 | 23 | protected convertFromValue(distance: number): Rawvalue { 24 | const numberString = format(distance, "000.0"); 25 | return [ 26 | Number(numberString.substring(0, 1)), 27 | Number(numberString.substring(1, 2)), 28 | Number(numberString.substring(2, 3)), 29 | Number(numberString.substring(4, 5)), 30 | ]; 31 | } 32 | 33 | protected convertToValue(rawValue: Rawvalue): Promise { 34 | const newValue = Number(String(rawValue[0]) + String(rawValue[1]) + String(rawValue[2]) + "." + String(rawValue[3])); 35 | if (newValue >= 360) { 36 | return Promise.resolve(null); 37 | } 38 | 39 | return Promise.resolve(newValue); 40 | } 41 | } -------------------------------------------------------------------------------- /kln90b/services/Timers.ts: -------------------------------------------------------------------------------- 1 | import {CalcTickable, TICK_TIME_CALC} from "../TickController"; 2 | import {Sensors} from "../Sensors"; 3 | import {FLT_TIMER_POWER, KLN90BUserSettings} from "../settings/KLN90BUserSettings"; 4 | import {DtPageState} from "../data/VolatileMemory"; 5 | import {UserSetting} from "@microsoft/msfs-sdk"; 6 | 7 | const SAVE_INTERVALL = 60000; 8 | 9 | export class Timers implements CalcTickable { 10 | private flightTimerSetting: UserSetting; 11 | private totalTimeSetting: UserSetting; 12 | private totalTime: number; 13 | 14 | private intSaveCount = 0; 15 | 16 | constructor(private sensors: Sensors, settings: KLN90BUserSettings, private state: DtPageState) { 17 | this.flightTimerSetting = settings.getSetting("flightTimer"); 18 | this.totalTimeSetting = settings.getSetting("totalTime"); 19 | this.totalTime = this.totalTimeSetting.get(); 20 | console.log("total time: ", this.totalTime); 21 | } 22 | 23 | public tick(): void { 24 | if (this.flightTimerSetting.get() === FLT_TIMER_POWER || this.sensors.in.gps.groundspeed >= 30) { 25 | this.state.flightTimer += TICK_TIME_CALC / 1000; 26 | if (this.state.departureTime === null) { 27 | this.state.departureTime = this.sensors.in.gps.timeZulu; 28 | } 29 | } 30 | this.totalTime += TICK_TIME_CALC / 1000; 31 | this.intSaveCount += TICK_TIME_CALC; 32 | if (this.intSaveCount >= SAVE_INTERVALL) { 33 | //we save this every 60 seconds 34 | this.totalTimeSetting.set(this.totalTime); 35 | this.intSaveCount = 0; 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Set4Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {SelectField} from "../../controls/selects/SelectField"; 6 | import {FLT_TIMER_POWER} from "../../settings/KLN90BUserSettings"; 7 | 8 | 9 | type Set4PageTypes = { 10 | flightTimer: SelectField; 11 | } 12 | 13 | /** 14 | * 4-13 15 | */ 16 | export class Set4Page extends SixLineHalfPage { 17 | 18 | public readonly cursorController; 19 | readonly children: UIElementChildren; 20 | 21 | readonly name: string = "SET 4"; 22 | 23 | constructor(props: PageProps) { 24 | super(props); 25 | 26 | 27 | const flightTimer = this.props.userSettings.getSetting("flightTimer").get(); 28 | 29 | this.children = new UIElementChildren({ 30 | flightTimer: new SelectField(["GS > 30kt ", "POWER IS ON"], flightTimer === FLT_TIMER_POWER ? 1 : 0, this.saveFlightTimer.bind(this)), 31 | }); 32 | 33 | this.cursorController = new CursorController(this.children); 34 | } 35 | 36 | public render(): VNode { 37 | return (
38 |               FLIGHT
39 |   TIMER
40 |  OPERATION
41 |
42 | RUN WHEN
43 | {this.children.get("flightTimer").render()} 44 |
); 45 | } 46 | 47 | private saveFlightTimer(flightTimer: number): void { 48 | this.props.userSettings.getSetting("flightTimer").set(flightTimer === 1); 49 | } 50 | 51 | 52 | } -------------------------------------------------------------------------------- /kln90b/data/navdata/UniqueIdentGenerator.ts: -------------------------------------------------------------------------------- 1 | import {FacilityClient, FacilitySearchType} from "@microsoft/msfs-sdk"; 2 | import {format} from "numerable"; 3 | 4 | /** 5 | * Generates a unique ident and uses alphabet letters at the end. Null if no unique ident was found 6 | * @param ident 7 | * @param facilityLoader 8 | */ 9 | export async function getUniqueIdent(ident: string, facilityLoader: FacilityClient): Promise { 10 | const start = ident.substring(0, 4); 11 | const existing = await facilityLoader.searchByIdentWithIcaoStructs(FacilitySearchType.All, start, 100); 12 | const SUFFIXES = ['', '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']; 13 | const existingIdents = existing.map(i => i.ident); 14 | for (const suffix of SUFFIXES) { 15 | if (!existingIdents.includes(start + suffix)) { 16 | return start + suffix; 17 | } 18 | } 19 | return null; 20 | } 21 | 22 | /** 23 | * Generates a unique ident, but with numbers at the end 24 | * @param ident Expects as VOR with three letters 25 | * @param facilityLoader 26 | */ 27 | export async function getUniqueIdentWithNumbers(ident: string, facilityLoader: FacilityClient): Promise { 28 | const start = ident; 29 | const existing = await facilityLoader.searchByIdentWithIcaoStructs(FacilitySearchType.All, start, 100); 30 | const existingIdents = existing.map(i => i.ident); 31 | for (let i = 0; i < 100; i++) { 32 | const checkIdent = start + format(i, "00"); 33 | if (!existingIdents.includes(checkIdent)) { 34 | return checkIdent; 35 | } 36 | } 37 | return null; 38 | } -------------------------------------------------------------------------------- /kln90b/services/FlightplanUtils.ts: -------------------------------------------------------------------------------- 1 | import {NauticalMiles} from "../data/Units"; 2 | import {GeoPoint, UnitType} from "@microsoft/msfs-sdk"; 3 | import {NavPageState} from "../data/VolatileMemory"; 4 | import {Flightplan, KLNFlightplanLeg} from "../data/flightplan/Flightplan"; 5 | 6 | export function calcDistToDestination(navState: NavPageState, futureLegs: KLNFlightplanLeg[]): NauticalMiles | null { 7 | const fplIdx = navState.activeWaypoint.getActiveFplIdx(); 8 | if (fplIdx === -1) { 9 | return navState.distToActive; 10 | } 11 | let dist = navState.distToActive!; 12 | for (let i = 1; i < futureLegs.length; i++) { 13 | const prev = futureLegs[i - 1]; 14 | const next = futureLegs[i]; 15 | dist += UnitType.GA_RADIAN.convertTo(new GeoPoint(prev.wpt.lat, prev.wpt.lon).distance(next.wpt), UnitType.NMILE); 16 | } 17 | return dist; 18 | } 19 | 20 | /** 21 | * Inserts a new waypoint into the flightplan. If the fpl is 0 and the flightplan is full, then it tries to delete the 22 | * first leg to make space (C-1) 23 | * @param fpl 24 | * @param navstate 25 | * @param idx 26 | * @param leg 27 | */ 28 | export function insertLegIntoFpl(fpl: Flightplan, navstate: NavPageState, idx: number, leg: KLNFlightplanLeg): void { 29 | if (fpl.idx !== 0 || fpl.getLegs().length < 30) { 30 | fpl.insertLeg(idx, leg); 31 | return; 32 | } 33 | 34 | const activeIdx = navstate.activeWaypoint.getActiveFplIdx(); 35 | if (activeIdx === -1 || activeIdx >= 2 || navstate.activeWaypoint.isDctNavigation() && activeIdx === 1) { 36 | fpl.deleteLeg(0); 37 | fpl.insertLeg(idx - 1, leg); 38 | } else { 39 | throw new Error("First waypoint is part of the active leg"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /kln90b/controls/displays/ActiveArrow.tsx: -------------------------------------------------------------------------------- 1 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 2 | import {FSComponent, ICAO, IcaoValue, NodeReference, VNode} from "@microsoft/msfs-sdk"; 3 | import {TickController} from "../../TickController"; 4 | import {NavPageState} from "../../data/VolatileMemory"; 5 | 6 | /** 7 | * 3-29, 3-42, 4-8 8 | */ 9 | export class ActiveArrow implements UiElement { 10 | readonly children = NO_CHILDREN; 11 | private ref: NodeReference = FSComponent.createRef(); 12 | 13 | 14 | constructor(public icao: IcaoValue | null, private navPageState: NavPageState) { 15 | } 16 | 17 | render(): VNode { 18 | return ( ); 19 | } 20 | 21 | public tick(blink: boolean): void { 22 | if (!TickController.checkRef(this.ref)) { 23 | return; 24 | } 25 | const activeWpt = this.navPageState.activeWaypoint.getActiveWpt(); 26 | if (activeWpt === null) { 27 | this.ref.instance.innerText = " "; 28 | this.ref.instance.classList.remove("blink"); 29 | } else { 30 | if (this.icao !== null && ICAO.valueEquals(this.icao, activeWpt.icaoStruct)) { 31 | this.ref.instance.innerText = "›"; 32 | //3-29 Waypoint alerting 33 | if (this.navPageState.waypointAlert && blink) { 34 | this.ref.instance.classList.add("blink"); 35 | } else { 36 | this.ref.instance.classList.remove("blink"); 37 | } 38 | } else { 39 | this.ref.instance.innerText = " "; 40 | this.ref.instance.classList.remove("blink"); 41 | } 42 | 43 | } 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /kln90b/services/TemporaryWaypointDeleter.ts: -------------------------------------------------------------------------------- 1 | import {KLNFacilityRepository} from "../data/navdata/KLNFacilityRepository"; 2 | import {EventBus, FacilityType, ICAO, IcaoValue, UserFacility} from "@microsoft/msfs-sdk"; 3 | import {PowerEvent} from "../PowerButton"; 4 | import {Flightplan} from "../data/flightplan/Flightplan"; 5 | import {TEMPORARY_WAYPOINT} from "../data/navdata/IcaoBuilder"; 6 | 7 | export class TemporaryWaypointDeleter { 8 | 9 | constructor(private readonly repo: KLNFacilityRepository, bus: EventBus, private readonly flightplans: Flightplan[]) { 10 | bus.getSubscriber().on("powerEvent").handle(this.deleteUnusedTemporaryWaypoints.bind(this)); 11 | } 12 | 13 | public static findUsageInFlightplans(icao: IcaoValue, flightplans: Flightplan[]): number | null { 14 | for (const flightplan of flightplans) { 15 | if (flightplan.getLegs().some(leg => ICAO.valueEquals(leg.wpt.icaoStruct, icao))) { 16 | return flightplan.idx; 17 | } 18 | } 19 | return null; 20 | } 21 | 22 | /** 23 | * 5-22 24 | * @private 25 | */ 26 | private deleteUnusedTemporaryWaypoints() { 27 | console.log("Deleting unused waypoints"); 28 | this.repo.forEach(fac => { 29 | const userFac = fac as UserFacility; 30 | //Region XY marks this as temporary. isTemporary can't be used, because that is not persisted 31 | if (userFac.icaoStruct.region === TEMPORARY_WAYPOINT && TemporaryWaypointDeleter.findUsageInFlightplans(userFac.icaoStruct, this.flightplans) === null) { 32 | console.log("Deleting unused waypoint:", userFac); 33 | this.repo.remove(userFac); 34 | } 35 | }, [FacilityType.USR]); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /kln90b/pages/TakehomePage.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {NO_CURSOR_CONTROLLER} from "./CursorController"; 3 | import {SixLinePage} from "./FourSegmentPage"; 4 | import {PageProps, UIElementChildren} from "./Page"; 5 | import {Blink} from "../controls/Blink"; 6 | import {FiveSegmentPage} from "./FiveSegmentPage"; 7 | import {SelfTestLeftPage} from "./left/SelfTestLeftPage"; 8 | import {SelfTestRightPage} from "./right/SelfTestRightPage"; 9 | 10 | 11 | type TakehomePageChildTypes = { 12 | ack: Blink; 13 | } 14 | 15 | 16 | export class TakehomePage extends SixLinePage { 17 | public readonly lCursorController = NO_CURSOR_CONTROLLER; 18 | public readonly rCursorController = NO_CURSOR_CONTROLLER; 19 | readonly children = new UIElementChildren( 20 | { 21 | ack: new Blink("ACKNOWLEDGE?"), 22 | }, 23 | ); 24 | 25 | constructor(props: PageProps) { 26 | super(props); 27 | } 28 | 29 | public render(): VNode { 30 | return (
31 |        WARNING:
32 |   SYSTEM IS IN TAKE-
33 |   HOME MODE:  DO NOT
34 |   USE FOR NAVIGATION
35 |
36 |      {this.children.get("ack").render()} 37 |
); 38 | } 39 | 40 | 41 | enter(): boolean { 42 | this.props.pageManager.setCurrentPage(FiveSegmentPage, { 43 | ...this.props, 44 | lPage: new SelfTestLeftPage(this.props), 45 | rPage: new SelfTestRightPage(this.props), 46 | }); 47 | return true; 48 | } 49 | 50 | 51 | isEnterAccepted(): boolean { 52 | return true; 53 | } 54 | } -------------------------------------------------------------------------------- /resources/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": [ 3 | { 4 | "path": "ContentInfo/falcon71-kln90b/Thumbnail.jpg", 5 | "size": 23518, 6 | "date": 133193178863126310 7 | }, 8 | { 9 | "path": "html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/KLN90B.css", 10 | "size": 3522, 11 | "date": 133830346924689159 12 | }, 13 | { 14 | "path": "html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/KLN90B.html", 15 | "size": 423, 16 | "date": 133174817281435761 17 | }, 18 | { 19 | "path": "html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/KLN90B.js", 20 | "size": 2833337, 21 | "date": 133830346924689159 22 | }, 23 | { 24 | "path": "html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/KLN90B.js.map", 25 | "size": 7610367, 26 | "date": 133830346924789154 27 | }, 28 | { 29 | "path": "html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/gps_ephemeris.json", 30 | "size": 32295, 31 | "date": 133240704899808958 32 | }, 33 | { 34 | "path": "html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/gps_sbas.json", 35 | "size": 2, 36 | "date": 133466044474079866 37 | }, 38 | { 39 | "path": "html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/kln90b-map.ttf", 40 | "size": 9076, 41 | "date": 133177526300000000 42 | }, 43 | { 44 | "path": "html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/kln90b.ttf", 45 | "size": 31180, 46 | "date": 133192037940000000 47 | }, 48 | { 49 | "path": "html_ui/Pages/VCockpit/Instruments/NavSystems/GPS/KLN90B/Assets/msa.json", 50 | "size": 239406, 51 | "date": 133181766582322541 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/NdbFreqEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {NumberEditorField} from "./EditorField"; 4 | import {format} from "numerable"; 5 | 6 | export class NdbFreqEditor extends Editor { 7 | 8 | constructor(bus: EventBus, value: number | null, enterCallback: (text: number) => void) { 9 | super(bus, [ 10 | NumberEditorField.createWithBlankMax(1), 11 | new NumberEditorField(), 12 | new NumberEditorField(), 13 | new NumberEditorField(), 14 | new NumberEditorField(), 15 | ], value, enterCallback); 16 | } 17 | 18 | public render(): VNode { 19 | return ( 20 | {this.editorFields[0].render()}{this.editorFields[1].render()}{this.editorFields[2].render()}{this.editorFields[3].render()}.{this.editorFields[4].render()} 21 | ); 22 | } 23 | 24 | protected convertFromValue(value: number): Rawvalue { 25 | const numberString = format(value, "0000.0"); 26 | 27 | return [ 28 | Number(numberString.substring(0, 1)), 29 | Number(numberString.substring(1, 2)), 30 | Number(numberString.substring(2, 3)), 31 | Number(numberString.substring(3, 4)), 32 | Number(numberString.substring(5, 6)), 33 | ]; 34 | } 35 | 36 | protected convertToValue(rawValue: Rawvalue): Promise { 37 | const newValue = Number(rawValue[0] + rawValue[1] + rawValue[2] + rawValue[3] + "." + rawValue[4]); 38 | if (newValue < 190 || newValue > 1750) { 39 | return Promise.resolve(null); 40 | } 41 | return Promise.resolve(newValue); 42 | } 43 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/VolumeFieldset.tsx: -------------------------------------------------------------------------------- 1 | import {SelectField} from "./SelectField"; 2 | import {FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {UiElement, UIElementChildren} from "../../pages/Page"; 4 | import {format} from "numerable"; 5 | 6 | 7 | type VolumeFieldsetTypes = { 8 | Volume10: SelectField; 9 | Volume1: SelectField; 10 | } 11 | 12 | const CHAR_SET = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; 13 | 14 | export class VolumeFieldset implements UiElement { 15 | 16 | 17 | readonly children: UIElementChildren; 18 | 19 | constructor(private volume: number, private readonly callback: (volume: number) => void) { 20 | const VolumeString = format(volume, "00"); 21 | this.children = new UIElementChildren({ 22 | Volume10: new SelectField(CHAR_SET, Number(VolumeString.substring(0, 1)), this.saveVolume10.bind(this)), 23 | Volume1: new SelectField(CHAR_SET, Number(VolumeString.substring(1, 2)), this.saveVolume1.bind(this)), 24 | }); 25 | } 26 | 27 | render(): VNode { 28 | return ( 29 | {this.children.get("Volume10").render()}{this.children.get("Volume1").render()}); 30 | } 31 | 32 | tick(blink: boolean): void { 33 | } 34 | 35 | 36 | private saveVolume10(newVolume10: number): void { 37 | const oldVolume = format(this.volume, "00"); 38 | this.volume = Number(newVolume10 + oldVolume.substring(1)); 39 | this.callback(this.volume); 40 | } 41 | 42 | private saveVolume1(newVolume1: number): void { 43 | const oldVolume = format(this.volume, "00"); 44 | this.volume = Number(oldVolume.substring(0, 1) + newVolume1); 45 | this.callback(this.volume); 46 | 47 | } 48 | 49 | 50 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/ElevationEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {NumberEditorField} from "./EditorField"; 4 | import {format} from "numerable"; 5 | 6 | export class ElevationEditor extends Editor { 7 | 8 | constructor(bus: EventBus, value: number | null, enterCallback: (text: number) => void) { 9 | super(bus, [ 10 | new NumberEditorField(), 11 | new NumberEditorField(), 12 | new NumberEditorField(), 13 | new NumberEditorField(), 14 | new NumberEditorField(), 15 | ], value, enterCallback); 16 | } 17 | 18 | public render(): VNode { 19 | return ( 20 | {this.editorFields[0].render()}{this.editorFields[1].render()}{this.editorFields[2].render()}{this.editorFields[3].render()}{this.editorFields[4].render()} 21 | ); 22 | } 23 | 24 | protected convertFromValue(altitude: number): Rawvalue { 25 | const numberString = format(altitude, "00000"); 26 | return [ 27 | Number(numberString.substring(0, 1)), 28 | Number(numberString.substring(1, 2)), 29 | Number(numberString.substring(2, 3)), 30 | Number(numberString.substring(3, 4)), 31 | Number(numberString.substring(4, 5)), 32 | ]; 33 | } 34 | 35 | protected convertToValue(rawValue: Rawvalue): Promise { 36 | const newValue = Number(String(rawValue[0]) + String(rawValue[1]) + String(rawValue[2]) + String(rawValue[3]) + String(rawValue[4])); 37 | return Promise.resolve(newValue); 38 | } 39 | } 40 | 41 | export class RunwayLengthEditor extends ElevationEditor { //They are both the same 42 | 43 | } 44 | -------------------------------------------------------------------------------- /kln90b/pages/VFROnlyPage.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {CursorController, NO_CURSOR_CONTROLLER} from "./CursorController"; 3 | import {FourSegmentPage, SixLinePage} from "./FourSegmentPage"; 4 | import {PageProps, UIElementChildren} from "./Page"; 5 | import {Button} from "../controls/Button"; 6 | import {AiracPage} from "./AiracPage"; 7 | import {ObswarningPage} from "./ObsWarningPage"; 8 | 9 | 10 | type VFROnlyPageChildTypes = { 11 | ack: Button; 12 | } 13 | 14 | 15 | export class VFROnlyPage extends SixLinePage { 16 | public readonly lCursorController = NO_CURSOR_CONTROLLER; 17 | readonly children = new UIElementChildren({ 18 | ack: new Button("ACKNOWLEDGE?", this.acknowledge.bind(this)), 19 | }); 20 | public readonly rCursorController = new CursorController(this.children); 21 | 22 | constructor(props: PageProps) { 23 | super(props); 24 | this.rCursorController.setCursorActive(true); 25 | } 26 | 27 | public render(): VNode { 28 | return (
29 |
30 | FOR VFR USE ONLY
31 |
, 32 |
33 |
34 | {this.children.get("ack").render()} 35 |
); 36 | } 37 | 38 | 39 | private acknowledge(): void { 40 | if (this.props.modeController.isObsModeActive()) { 41 | this.props.pageManager.setCurrentPage(FourSegmentPage, { 42 | ...this.props, 43 | page: new ObswarningPage(this.props), 44 | }); 45 | } else { 46 | this.props.pageManager.setCurrentPage(FourSegmentPage, { 47 | ...this.props, 48 | page: new AiracPage(this.props), 49 | }); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /kln90b/data/navdata/KLNMagvar.ts: -------------------------------------------------------------------------------- 1 | import {LatLonInterface, MagVar} from "@microsoft/msfs-sdk"; 2 | import {Degrees} from "../Units"; 3 | import {CalcTickable} from "../../TickController"; 4 | import {Sensors} from "../../Sensors"; 5 | import {NavPageState} from "../VolatileMemory"; 6 | 7 | export class KLNMagvar implements CalcTickable { 8 | 9 | private dbMagvar: number = 0; 10 | private lat: number = 0; 11 | 12 | 13 | constructor(private readonly sensors: Sensors, private readonly navState: NavPageState) { 14 | } 15 | 16 | 17 | /** 18 | * 5-44 19 | */ 20 | public isMagvarValid() { 21 | return this.lat <= 74 && this.lat >= -60; 22 | } 23 | 24 | public trueToMag(tru: Degrees, magvar?: number): Degrees; 25 | public trueToMag(tru: Degrees | null, magvar?: number): Degrees | null; 26 | public trueToMag(tru: Degrees | null, magvar: number = this.getCurrentMagvar()): Degrees | null { 27 | if (tru === null) { 28 | return null; 29 | } 30 | return MagVar.trueToMagnetic(tru, magvar); 31 | } 32 | 33 | public magToTrue(mag: Degrees, magvar: number = this.getCurrentMagvar()): Degrees { 34 | return MagVar.magneticToTrue(mag, magvar); 35 | } 36 | 37 | public getCurrentMagvar(): number { 38 | return this.isMagvarValid() ? this.dbMagvar : this.navState.userMagvar; 39 | } 40 | 41 | public getMagvarForCoordinates(coords: LatLonInterface): number { 42 | return MagVar.get(coords); 43 | } 44 | 45 | 46 | public tick(): void { 47 | const coords = this.sensors.in.gps.coords; 48 | this.lat = coords.lat; 49 | if (this.isMagvarValid()) { 50 | this.dbMagvar = MagVar.get(coords); 51 | this.navState.userMagvar = 0; //Let's reset this to true north every time we cross 74° 52 | } 53 | } 54 | 55 | 56 | } -------------------------------------------------------------------------------- /kln90b/settings/UserFlightplanLoaderV2.ts: -------------------------------------------------------------------------------- 1 | import {DefaultUserSettingManager, EventBus, FacilityClient, ICAO} from "@microsoft/msfs-sdk"; 2 | import {KLN90BUserFlightplansSettings, KLN90BUserFlightplansTypes} from "./KLN90BUserFlightplans"; 3 | import {Flightplan} from "../data/flightplan/Flightplan"; 4 | import {MessageHandler} from "../data/MessageHandler"; 5 | import {Flightplanloader} from "../services/Flightplanloader"; 6 | import {UserFlightplanLoader} from "./UserFlightplanPersistor"; 7 | 8 | 9 | export class UserFlightplanLoaderV2 extends Flightplanloader implements UserFlightplanLoader { 10 | private manager: DefaultUserSettingManager; 11 | 12 | constructor(bus: EventBus, facilityLoader: FacilityClient, messageHandler: MessageHandler) { 13 | super(bus, facilityLoader, messageHandler); 14 | this.manager = KLN90BUserFlightplansSettings.getManager(bus); 15 | } 16 | 17 | public restoreAllFlightplan(): Promise { 18 | const promises = Array(26).fill(undefined).map((_, i) => this.restoreFlightplan.bind(this)(i)); 19 | return Promise.all(promises); 20 | } 21 | 22 | private async restoreFlightplan(idx: number): Promise { 23 | try { 24 | const setting = this.manager.getSetting(`fpl${idx}`); 25 | 26 | const serialized = setting.get(); 27 | console.log(`restoring flightplan ${idx}`, serialized); 28 | 29 | if (serialized === "") { 30 | return new Flightplan(idx, [], this.bus); 31 | } 32 | 33 | const serializedLegs = serialized.match(/.{1,19}/g)!.map(ICAO.stringV2ToValue); 34 | return await this.loadIcaos(serializedLegs, idx); 35 | } catch (e) { 36 | console.log(`Error restoring fpl ${idx}`, e); 37 | throw e; 38 | } 39 | } 40 | 41 | 42 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/DurationDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {Seconds} from "../../data/Units"; 5 | import {format} from "numerable"; 6 | 7 | /** 8 | * Displays a duration in the format HH:MM 9 | */ 10 | export class DurationDisplay implements UiElement { 11 | readonly children = NO_CHILDREN; 12 | public isVisible = true; 13 | private readonly ref: NodeReference = FSComponent.createRef(); 14 | 15 | constructor(public time: Seconds | null = null) { 16 | } 17 | 18 | render(): VNode { 19 | return ({this.formatTime()}); 20 | } 21 | 22 | public tick(blink: boolean): void { 23 | if (!TickController.checkRef(this.ref)) { 24 | return; 25 | } 26 | if (this.isVisible) { 27 | this.ref!.instance.classList.remove("d-none"); 28 | } else { 29 | this.ref!.instance.classList.add("d-none"); 30 | } 31 | this.ref.instance.innerText = this.formatTime(); 32 | } 33 | 34 | private formatTime(): string { 35 | return formatDuration(this.time); 36 | } 37 | } 38 | 39 | export function formatDuration(duration: Seconds | null): string { 40 | if (duration === null) { 41 | return "--:--"; 42 | } 43 | const totalMinutes = duration / 60; 44 | if (totalMinutes / 60 >= 100) { 45 | return "--:--"; 46 | } 47 | const hours = Math.floor(totalMinutes / 60); 48 | 49 | 50 | const minutes = totalMinutes % 60; 51 | 52 | if (hours === 0) { 53 | return ` :${format(minutes, "00")}`; 54 | } else { 55 | return `${hours.toString().padStart(2, " ")}:${format(minutes, "00")}`; 56 | } 57 | } -------------------------------------------------------------------------------- /kln90b/services/HtAboveAirportAlert.ts: -------------------------------------------------------------------------------- 1 | import {CalcTickable} from "../TickController"; 2 | import {NavPageState} from "../data/VolatileMemory"; 3 | import {KLN90PlaneSettings} from "../settings/KLN90BPlaneSettings"; 4 | import {Facility, FacilityType, FacilityUtils} from "@microsoft/msfs-sdk"; 5 | import {Sensors} from "../Sensors"; 6 | import {KLN90BUserSettings} from "../settings/KLN90BUserSettings"; 7 | import {LONG_BEEP_ID, SHORT_BEEP_ID} from "./AudioGenerator"; 8 | 9 | /** 10 | * 3-58 11 | */ 12 | export class HtAboveAirportAlert implements CalcTickable { 13 | 14 | private alerted: Facility | null = null; 15 | 16 | 17 | constructor(private navPageState: NavPageState, private settings: KLN90PlaneSettings, private sensors: Sensors, private userSettings: KLN90BUserSettings) { 18 | } 19 | 20 | public tick(): void { 21 | const activeWpt = this.navPageState.activeWaypoint.getActiveWpt(); 22 | const indicatedAlt = this.sensors.in.airdata.getIndicatedAlt(); 23 | if (!this.settings.output.altitudeAlertEnabled || indicatedAlt === null || activeWpt === null) { 24 | return; 25 | } 26 | 27 | const enabled = this.userSettings.getSetting("htAboveAptEnabled").get(); 28 | 29 | if (!enabled || !FacilityUtils.isFacilityType(activeWpt, FacilityType.Airport)) { 30 | return; 31 | } 32 | 33 | const offset = this.userSettings.getSetting("airspaceAlertBuffer").get(); 34 | 35 | 36 | if (this.navPageState.distToActive! <= 5 && indicatedAlt <= (activeWpt as any).altitude + offset) { 37 | if (this.alerted !== activeWpt) { 38 | this.sensors.out.audioGenerator.beepPattern([SHORT_BEEP_ID, LONG_BEEP_ID, SHORT_BEEP_ID]); 39 | this.alerted = activeWpt; 40 | } 41 | } else { 42 | this.alerted = null; 43 | } 44 | 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/RoundedDistanceDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {NauticalMiles} from "../../data/Units"; 5 | import {format} from "numerable"; 6 | 7 | export const enum Alignment { 8 | left, 9 | right, 10 | } 11 | 12 | /** 13 | * 4-45 14 | * The distance display on the D/T page seems somewhat special. It shows four dashes when empty, but then only display 15 | * three unroundet digits when filled. 16 | */ 17 | export class RoundedDistanceDisplay implements UiElement { 18 | readonly children = NO_CHILDREN; 19 | public isVisible = true; 20 | private readonly ref: NodeReference = FSComponent.createRef(); 21 | 22 | constructor(private alignment: Alignment, public distance: NauticalMiles | null = null) { 23 | } 24 | 25 | render(): VNode { 26 | return ({this.formatDistance()}); 27 | } 28 | 29 | public tick(blnk: boolean): void { 30 | if (!TickController.checkRef(this.ref)) { 31 | return; 32 | } 33 | 34 | if (this.isVisible) { 35 | this.ref!.instance.classList.remove("d-none"); 36 | } else { 37 | this.ref!.instance.classList.add("d-none"); 38 | } 39 | this.ref.instance.innerText = this.formatDistance(); 40 | } 41 | 42 | private formatDistance(): string { 43 | if (this.distance === null) { 44 | return "----"; 45 | } 46 | const rounded = Math.round(this.distance); 47 | 48 | return this.align(format(rounded, "0").padStart(3, " ")); 49 | } 50 | 51 | private align(value: string): string { 52 | return this.alignment === Alignment.right ? value.padStart(4, " ") : value.padEnd(4, " "); 53 | } 54 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/CreateWaypointMessage.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {UiElement, UIElementChildren} from "../../pages/Page"; 3 | import {Button} from "../Button"; 4 | import {TickController} from "../../TickController"; 5 | 6 | 7 | type CreateWaypointMessageTypes = { 8 | userPos: Button, 9 | presPos: Button, 10 | } 11 | 12 | export class CreateWaypointMessage implements UiElement { 13 | 14 | 15 | readonly children: UIElementChildren; 16 | 17 | protected readonly ref: NodeReference = FSComponent.createRef(); 18 | 19 | private isVisible = false; 20 | 21 | constructor(createUserPosHandler: () => void, createPresentPosHandler: () => void) { 22 | this.children = new UIElementChildren({ 23 | userPos: new Button("USER POS?", createUserPosHandler), 24 | presPos: new Button("PRES POS?", createPresentPosHandler), 25 | }); 26 | } 27 | 28 | render(): VNode { 29 | return (
30 |
31 | CREATE NEW
32 | WPT AT:
33 | {this.children.get("userPos").render()}
34 | {this.children.get("presPos").render()} 35 |
); 36 | } 37 | 38 | tick(blink: boolean): void { 39 | if (!TickController.checkRef(this.ref)) { 40 | return; 41 | } 42 | if (this.isVisible) { 43 | this.ref.instance.classList.remove("d-none"); 44 | } else { 45 | this.ref.instance.classList.add("d-none"); 46 | } 47 | } 48 | 49 | public setVisible(visible: boolean) { 50 | this.isVisible = visible; 51 | this.children.get("userPos").isReadonly = !visible; 52 | this.children.get("presPos").isReadonly = !visible; 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /kln90b/pages/AiracPage.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {CursorController, NO_CURSOR_CONTROLLER} from "./CursorController"; 3 | import {SixLinePage} from "./FourSegmentPage"; 4 | import {PageProps, UIElementChildren} from "./Page"; 5 | import {Button} from "../controls/Button"; 6 | 7 | 8 | type AiracPageChildTypes = { 9 | ack: Button; 10 | } 11 | 12 | 13 | export class AiracPage extends SixLinePage { 14 | public readonly lCursorController = NO_CURSOR_CONTROLLER; 15 | readonly children = new UIElementChildren({ 16 | ack: new Button("ACKNOWLEDGE?", this.acknowledge.bind(this)), 17 | }); 18 | public readonly rCursorController = new CursorController(this.children); 19 | 20 | constructor(props: PageProps) { 21 | super(props); 22 | this.rCursorController.setCursorActive(true); 23 | } 24 | 25 | public render(): VNode { 26 | return this.props.database.isAiracCurrent() ? 27 | (
28 |                  INTERNATIONAL
29 |    DATA BASE EXPIRES
30 |        {this.props.database.expirationDateString}
31 |
32 |
33 |       {this.children.get("ack").render()} 34 |
) : 35 | (
36 |                  INTERNATIONAL
37 |    DATA BASE EXPIRED
38 |        {this.props.database.expirationDateString}
39 |    ALL DATA MUST BE
40 |  CONFIRMED BEFORE USE
41 |      {this.children.get("ack").render()} 42 |
); 43 | } 44 | 45 | 46 | private acknowledge(): void { 47 | this.props.pageManager.startMainPage(this.props); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/DistanceDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {NauticalMiles} from "../../data/Units"; 5 | import {format} from "numerable"; 6 | 7 | /** 8 | * Displays a formatted distance 9 | */ 10 | export class DistanceDisplay implements UiElement { 11 | readonly children = NO_CHILDREN; 12 | public isVisible = true; 13 | private readonly ref: NodeReference = FSComponent.createRef(); 14 | 15 | /** 16 | * Examples for length: 17 | * 3: 3-32 XTK nav 3 page 18 | * 4: 3-31 nav 1 page 19 | * 6: 3-8 NAV 2 page 20 | * @param length 21 | * @param distance 22 | */ 23 | constructor(public length: number, public distance: NauticalMiles | null = null) { 24 | } 25 | 26 | render(): VNode { 27 | return ({this.formatDistance()}); 28 | } 29 | 30 | public tick(blink: boolean): void { 31 | if (!TickController.checkRef(this.ref)) { 32 | return; 33 | } 34 | 35 | if (this.isVisible) { 36 | this.ref!.instance.classList.remove("d-none"); 37 | } else { 38 | this.ref!.instance.classList.add("d-none"); 39 | } 40 | this.ref.instance.innerText = this.formatDistance(); 41 | } 42 | 43 | private formatDistance(): string { 44 | if (this.distance === null) { 45 | return ".-".padStart(this.length, "-"); 46 | } 47 | 48 | const cutoff = 10 ** (this.length - 2); 49 | if (this.distance >= cutoff) { //Number is too large for decimals 50 | return format(this.distance, "00").padStart(this.length, " "); 51 | } else { 52 | return format(this.distance, "0.0").padStart(this.length, " "); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/SpeedDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {Knots} from "../../data/Units"; 5 | import {format} from "numerable"; 6 | 7 | /** 8 | * Displays a formatted speed 9 | */ 10 | export class SpeedDisplay implements UiElement { 11 | readonly children = NO_CHILDREN; 12 | private readonly ref: NodeReference = FSComponent.createRef(); 13 | 14 | constructor(public speed: Knots | null = null) { 15 | } 16 | 17 | render(): VNode { 18 | return ({this.formatSpeed()}); 19 | } 20 | 21 | public tick(blnk: boolean): void { 22 | if (!TickController.checkRef(this.ref)) { 23 | return; 24 | } 25 | this.ref.instance.innerText = this.formatSpeed(); 26 | } 27 | 28 | private formatSpeed(): string { 29 | if (this.speed === null) { 30 | return "---"; 31 | } 32 | const clamped = Utils.Clamp(this.speed, 0, 999); 33 | return format(clamped, "0").padStart(3, " "); 34 | } 35 | } 36 | 37 | 38 | export class MachDisplay implements UiElement { 39 | readonly children = NO_CHILDREN; 40 | private readonly ref: NodeReference = FSComponent.createRef(); 41 | 42 | constructor(public mach: number) { 43 | } 44 | 45 | render(): VNode { 46 | return ({this.formatMach()}); 47 | } 48 | 49 | public tick(blnk: boolean): void { 50 | if (!TickController.checkRef(this.ref)) { 51 | return; 52 | } 53 | this.ref.instance.innerText = this.formatMach(); 54 | } 55 | 56 | private formatMach(): string { 57 | const clamped = Utils.Clamp(this.mach, 0, 0.99); 58 | return format(clamped, "0.00").substring(1); 59 | } 60 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Set7Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {TextDisplay} from "../../controls/displays/TextDisplay"; 6 | import {SelectField} from "../../controls/selects/SelectField"; 7 | import {BARO_UNIT_HPA} from "../../settings/KLN90BUserSettings"; 8 | 9 | 10 | type Set7PageTypes = { 11 | baroUnit: SelectField; 12 | baroUnitText: TextDisplay; 13 | } 14 | 15 | /** 16 | * 5-10 17 | */ 18 | export class Set7Page extends SixLineHalfPage { 19 | 20 | public readonly cursorController; 21 | readonly children: UIElementChildren; 22 | 23 | readonly name: string = "SET 7"; 24 | 25 | constructor(props: PageProps) { 26 | super(props); 27 | 28 | 29 | const baroUnit = this.props.userSettings.getSetting("barounit").get(); 30 | 31 | this.children = new UIElementChildren({ 32 | baroUnit: new SelectField(["\" ", "MB"], baroUnit === BARO_UNIT_HPA ? 1 : 0, this.saveBaroUnit.bind(this)), 33 | baroUnitText: new TextDisplay(baroUnit === BARO_UNIT_HPA ? "MILLIBARS" : " INCHES"), 34 | }); 35 | 36 | this.cursorController = new CursorController(this.children); 37 | } 38 | 39 | public render(): VNode { 40 | return (
41 |              BARO SET
42 |   UNITS
43 |
44 |     {this.children.get("baroUnit").render()}
45 |
46 |  {this.children.get("baroUnitText").render()} 47 |
); 48 | } 49 | 50 | private saveBaroUnit(baroUnit: number): void { 51 | this.children.get("baroUnitText").text = baroUnit === 0 ? " INCHES" : "MILLIBARS"; 52 | 53 | this.props.userSettings.getSetting("barounit").set(baroUnit === 0); 54 | } 55 | 56 | 57 | } -------------------------------------------------------------------------------- /kln90b/data/Conversions.ts: -------------------------------------------------------------------------------- 1 | import {Celsius, Feet, Inhg, Kelvin, Knots} from "./Units"; 2 | import {UnitType} from "@microsoft/msfs-sdk"; 3 | 4 | /** 5 | * https://edwilliams.org/avform147.htm#Altimetry 6 | * @param pressureAlt 7 | * @param baro 8 | */ 9 | export function pressureAlt2IndicatedAlt(pressureAlt: Feet, baro: Inhg): Feet { 10 | return pressureAlt - pressureAltCorr(baro); 11 | } 12 | 13 | /** 14 | * https://edwilliams.org/avform147.htm#Altimetry 15 | * @param indicatedAlt 16 | * @param baro 17 | */ 18 | export function indicatedAlt2PressureAlt(indicatedAlt: Feet, baro: Inhg): Feet { 19 | return indicatedAlt + pressureAltCorr(baro); 20 | } 21 | 22 | function pressureAltCorr(baro: Inhg): Feet { 23 | return 145442.2 * (1 - (baro / 29.92126) ** 0.190261); 24 | } 25 | 26 | /** 27 | * https://edwilliams.org/avform147.htm#Altimetry 28 | * @param pressureAlt 29 | * @param sat 30 | */ 31 | export function pressureAlt2DensityAlt(pressureAlt: Feet, sat: Celsius): Feet { 32 | const T_s = 15 - 0.0019812 * pressureAlt; 33 | return pressureAlt + 118.6 * (sat - T_s); 34 | } 35 | 36 | /** 37 | * https://edwilliams.org/avform147.htm#Mach 38 | * @param mach 39 | * @param tat 40 | */ 41 | export function mach2Tas(mach: number, tat: Celsius) { 42 | const K = 1; 43 | const OAT = calcOat(tat, mach, K); 44 | const CS = 38.967854 * Math.sqrt(UnitType.CELSIUS.convertTo(OAT, UnitType.KELVIN)); 45 | return mach * CS; 46 | } 47 | 48 | export function cas2Mach(cas: Knots, pressureAlt: Feet) { 49 | const P_0 = 29.92126; 50 | const CS_0 = 661.4786; 51 | const DP = P_0 * ((1 + 0.2 * (cas / CS_0) ** 2) ** 3.5 - 1); 52 | 53 | const P = P_0 * (1 - 6.8755856 * 10 ** -6 * pressureAlt) ** 5.2558797; 54 | 55 | return (5 * ((DP / P + 1) ** (2 / 7) - 1)) ** 0.5; 56 | } 57 | 58 | 59 | 60 | 61 | function calcOat(tat: Celsius, mach: number, recoveryFactor: number) { 62 | return UnitType.KELVIN.convertTo(UnitType.CELSIUS.convertTo(tat, UnitType.KELVIN) / (1 + 0.2 * recoveryFactor * mach ** 2), UnitType.CELSIUS); 63 | } -------------------------------------------------------------------------------- /kln90b/settings/UserFlightplanLoaderV1.ts: -------------------------------------------------------------------------------- 1 | import {DefaultUserSettingManager, EventBus, FacilityClient, ICAO} from "@microsoft/msfs-sdk"; 2 | import {KLN90BUserFlightplansSettings, KLN90BUserFlightplansTypes} from "./KLN90BUserFlightplans"; 3 | import {Flightplan} from "../data/flightplan/Flightplan"; 4 | import {MessageHandler} from "../data/MessageHandler"; 5 | import {Flightplanloader} from "../services/Flightplanloader"; 6 | import {UserFlightplanLoader} from "./UserFlightplanPersistor"; 7 | 8 | 9 | export class UserFlightplanLoaderV1 extends Flightplanloader implements UserFlightplanLoader { 10 | private manager: DefaultUserSettingManager; 11 | 12 | constructor(bus: EventBus, facilityLoader: FacilityClient, messageHandler: MessageHandler) { 13 | super(bus, facilityLoader, messageHandler); 14 | this.manager = KLN90BUserFlightplansSettings.getManager(bus); 15 | } 16 | 17 | public restoreAllFlightplan(): Promise { 18 | const promises = Array(26).fill(undefined).map((_, i) => this.restoreFlightplan.bind(this)(i)); 19 | return Promise.all(promises); 20 | } 21 | 22 | private async restoreFlightplan(idx: number): Promise { 23 | try { 24 | if (idx === 0) { 25 | //Flight plan 0 was not saved in version 1 26 | return new Flightplan(0, [], this.bus); 27 | } 28 | 29 | const setting = this.manager.getSetting(`fpl${idx - 1}`); 30 | 31 | const serialized = setting.get(); 32 | console.log(`restoring flightplan ${idx}`, serialized); 33 | 34 | if (serialized === "") { 35 | return new Flightplan(idx, [], this.bus); 36 | } 37 | 38 | const serializedLegs = serialized.match(/.{1,12}/g)!.map(ICAO.stringV1ToValue); 39 | return await this.loadIcaos(serializedLegs, idx); 40 | } catch (e) { 41 | console.log(`Error restoring fpl ${idx}`, e); 42 | throw e; 43 | } 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/SuperNav5RangeSelector.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, UserSetting} from "@microsoft/msfs-sdk"; 2 | import {SelectField} from "./SelectField"; 3 | import {TickController} from "../../TickController"; 4 | import {NavPageState} from "../../data/VolatileMemory"; 5 | 6 | const RANGES = ["AUTO", "1 ", "2 ", "3 ", "5 ", "10 ", "15 ", "20 ", "25 ", "30 ", "40 ", "60 ", "80 ", "100 ", "120 ", "160 ", "240 ", "320 ", "480 ", "1000"]; //I have no idea, which ranges the device supports 7 | 8 | export class SuperNav5RangeSelector extends SelectField { 9 | 10 | 11 | private constructor(private rangeSetting: UserSetting, private navState: NavPageState, changedCallback: (value: number) => void) { 12 | super(RANGES, rangeSetting.get() === 0 ? 0 : RANGES.indexOf(rangeSetting.get().toString().padEnd(4, " ")), changedCallback); 13 | } 14 | 15 | public static build(rangeSetting: UserSetting, navState: NavPageState): SuperNav5RangeSelector { 16 | return new SuperNav5RangeSelector(rangeSetting, navState, (range) => this.saveRange(rangeSetting, range)); 17 | } 18 | 19 | private static saveRange(rangeSetting: UserSetting, rangeIdx: number): void { 20 | const range = rangeIdx === 0 ? 0 : Number(RANGES[rangeIdx]); 21 | rangeSetting.set(range); 22 | } 23 | 24 | /** 25 | * 3-35 26 | * @param blink 27 | */ 28 | tick(blink: boolean): void { 29 | if (!TickController.checkRef(this.ref)) { 30 | return; 31 | } 32 | 33 | if (this.isFocused) { 34 | this.ref.instance.textContent = this.valueSet[this.value]; 35 | this.ref!.instance.classList.add("inverted"); 36 | } else { 37 | this.ref!.instance.classList.remove("inverted"); 38 | if (this.value === 0) { 39 | this.ref.instance.textContent = this.navState.superNav5ActualRange.toString(); 40 | } else { 41 | this.ref.instance.textContent = this.valueSet[this.value]; 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /kln90b/services/KLNNavmath.ts: -------------------------------------------------------------------------------- 1 | import {GeoPoint, LatLonInterface} from "@microsoft/msfs-sdk"; 2 | import {Degrees, Knots} from "../data/Units"; 3 | import {HOURS_TO_SECONDS, MAX_BANK_ANGLE} from "../data/navdata/NavCalculator"; 4 | 5 | const GEOPOINTCACHE = new GeoPoint(0, 0); 6 | 7 | //We assume that the aircraft can change it's bank angle by 5°/s 8 | const BANK_ANGLE_CHANGE_RATE = 5; 9 | 10 | /** 11 | * https://edwilliams.org/avform147.htm#Intermediate 12 | * @param coord0 13 | * @param coord1 14 | * @param f 15 | */ 16 | export function intermediatePoint(coord0: LatLonInterface, coord1: LatLonInterface, f: number): GeoPoint { 17 | const lat1 = coord0.lat * Avionics.Utils.DEG2RAD; 18 | const lat2 = coord1.lat * Avionics.Utils.DEG2RAD; 19 | const lon1 = coord0.lon * Avionics.Utils.DEG2RAD; 20 | const lon2 = coord1.lon * Avionics.Utils.DEG2RAD; 21 | GEOPOINTCACHE.set(coord0); 22 | const d = GEOPOINTCACHE.distance(coord1); 23 | const A = (Math.sin(1 - f) * d) / Math.sin(d); 24 | const B = Math.sin(f * d) / Math.sin(d); 25 | const x = A * Math.cos(lat1) * Math.cos(lon1) + B * Math.cos(lat2) * Math.cos(lon2); 26 | const y = A * Math.cos(lat1) * Math.sin(lon1) + B * Math.cos(lat2) * Math.sin(lon2); 27 | const z = A * Math.sin(lat1); 28 | const lat = Math.atan2(z, Math.sqrt((x ** 2) + (y ** 2))); 29 | const lon = Math.atan2(y, x); 30 | return new GeoPoint(lat * Avionics.Utils.RAD2DEG, lon * Avionics.Utils.RAD2DEG); 31 | } 32 | 33 | /** 34 | * https://edwilliams.org/avform147.htm#Turns 35 | * @param speed 36 | * @private 37 | */ 38 | export function bankeAngleForStandardTurn(speed: Knots) { 39 | return Math.min(57.3 * Math.atan(speed / 362.1), MAX_BANK_ANGLE); 40 | } 41 | 42 | /** 43 | * It is impossible to turn from 0° to 25° bank angle instantly. This calculates the distance we need to add to 44 | * achieve the desired bank angle change assuming a change of 5°/s 45 | * @param bankAngleChange 46 | * @param speed 47 | */ 48 | export function distanceToAchieveBankAngleChange(bankAngleChange: Degrees, speed: Knots) { 49 | return bankAngleChange / BANK_ANGLE_CHANGE_RATE * speed / HOURS_TO_SECONDS; 50 | } -------------------------------------------------------------------------------- /kln90b/controls/WaypointDeleteListItem.tsx: -------------------------------------------------------------------------------- 1 | import {Facility, FSComponent} from '@microsoft/msfs-sdk'; 2 | import {PageProps} from '../pages/Page'; 3 | import {StatusLineMessageEvents} from "./StatusLine"; 4 | import {ListItemProps, SimpleListItem} from "./ListItem"; 5 | import {MainPage} from "../pages/MainPage"; 6 | import {WaypointConfirmPage} from "../pages/right/WaypointConfirmPage"; 7 | import {SixLineHalfPage} from "../pages/FiveSegmentPage"; 8 | 9 | 10 | export class WaypointDeleteListItem extends SimpleListItem { 11 | public constructor(props: ListItemProps & PageProps, private parent: SixLineHalfPage) { 12 | super(props); 13 | } 14 | 15 | public setFocused(focused: boolean) { 16 | if (this.isEntered) { 17 | const mainPage = this.getPageProps().pageManager.getCurrentPage() as MainPage; 18 | mainPage.popRightPage(); 19 | } 20 | super.setFocused(focused); 21 | } 22 | 23 | clear(): boolean { 24 | if (this.props.onDelete === undefined) { 25 | return false; 26 | } 27 | if (this.isEntered) { //4-5 delete can be cancelled by pressing clear again 28 | const mainPage = this.getPageProps().pageManager.getCurrentPage() as MainPage; 29 | mainPage.popRightPage(); 30 | this.isEntered = false; 31 | return true; 32 | } 33 | 34 | if (this.props.onBeforeDelete !== undefined) { 35 | const res = this.props.onBeforeDelete(this.props.value); 36 | if (res !== null) { 37 | this.props.bus.getPublisher().pub("statusLineMessage", res); 38 | return true; 39 | } 40 | } 41 | WaypointConfirmPage.showWaypointconfirmation({ 42 | ...this.getPageProps(), 43 | facility: this.props.value, 44 | }, this.parent); //We are the left page, that means we get priority and we will see the enter from the list 45 | 46 | 47 | this.isEntered = true; 48 | return true; 49 | } 50 | 51 | private getPageProps(): PageProps { 52 | return this.props as any; 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /kln90b/services/AudioGenerator.ts: -------------------------------------------------------------------------------- 1 | import {EventBus, SoundServer, SoundServerController} from "@microsoft/msfs-sdk"; 2 | import {KLN90PlaneSettings} from "../settings/KLN90BPlaneSettings"; 3 | import {StatusLineMessageEvents} from "../controls/StatusLine"; 4 | 5 | export const SHORT_BEEP_ID = "kln_short_beep"; 6 | export const LONG_BEEP_ID = "kln_long_beep"; 7 | 8 | 9 | /** 10 | * For this to work, the aircraft must have an entry with "tone_altitude_alert_default" defined in the section AvionicSounds of the sound.xml! 11 | */ 12 | export class AudioGenerator { 13 | 14 | 15 | private readonly soundController: SoundServerController; 16 | private readonly soundServer: SoundServer; 17 | 18 | private beeps: string[] = []; 19 | 20 | constructor(private bus: EventBus, private settings: KLN90PlaneSettings) { 21 | this.soundController = new SoundServerController(bus); 22 | this.soundServer = new SoundServer(bus); 23 | } 24 | 25 | /** 26 | * Plays the specified amount of short beeps 27 | * @param numBeeps 28 | */ 29 | public shortBeeps(numBeeps: number) { 30 | this.beepPattern(Array(numBeeps).fill(SHORT_BEEP_ID)); 31 | } 32 | 33 | 34 | /** 35 | * Plays the pattern of beeps. Expects either SHORT_BEEP_ID or LONG_BEEP_ID 36 | * @param beeps 37 | */ 38 | public beepPattern(beeps: string[]) { 39 | if (!this.settings.output.altitudeAlertEnabled) { 40 | return; 41 | } 42 | if (this.settings.debugMode) { 43 | this.bus.getPublisher().pub("statusLineMessage", `BEEP: ${beeps.length}` as any) 44 | } 45 | this.beeps = beeps; 46 | this.doBeep(); 47 | } 48 | 49 | /** 50 | * A callback for when sounds are done playing. This is needed to support the sound server. 51 | * @param soundEventId The sound that got played. 52 | */ 53 | public onSoundEnd(soundEventId: Name_Z): void { 54 | this.soundServer.onSoundEnd(soundEventId); 55 | this.doBeep(); 56 | } 57 | 58 | private doBeep() { 59 | const beep = this.beeps.pop(); 60 | if (beep) { 61 | this.soundController.playSound(beep); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/TimeEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 3 | import {NumberEditorField} from "./EditorField"; 4 | import {TimeStamp} from "../../data/Time"; 5 | import {format} from "numerable"; 6 | import {TickController} from "../../TickController"; 7 | 8 | export class TimeEditor extends Editor { 9 | 10 | private colonRef: NodeReference = FSComponent.createRef(); 11 | constructor(bus: EventBus, value: TimeStamp | null, enterCallback: (text: TimeStamp) => void) { 12 | super(bus, [ 13 | NumberEditorField.createWithMinMax(0, 23), 14 | NumberEditorField.createWithMinMax(0, 5), 15 | new NumberEditorField(), 16 | ], value, enterCallback); 17 | } 18 | 19 | public render(): VNode { 20 | return ( 21 | {this.editorFields[0].render()}:{this.editorFields[1].render()}{this.editorFields[2].render()} 23 | ); 24 | } 25 | 26 | protected convertFromValue(value: TimeStamp): Rawvalue { 27 | const stringMinutes = format(value.getMinutes(), "00"); 28 | 29 | return [ 30 | value.getHours(), 31 | Number(stringMinutes.substring(0, 1)), 32 | Number(stringMinutes.substring(1, 2)), 33 | ]; 34 | } 35 | 36 | 37 | protected convertToValue(rawValue: Rawvalue): Promise { 38 | if (this.value === null) { 39 | return Promise.resolve(TimeStamp.createTime(rawValue[0], rawValue[1] * 10 + rawValue[2])); 40 | } else { 41 | return Promise.resolve(this.value!.withTime(rawValue[0], rawValue[1] * 10 + rawValue[2])); 42 | } 43 | 44 | } 45 | 46 | 47 | tick(blink: boolean): void { 48 | super.tick(blink); 49 | if (!TickController.checkRef(this.colonRef)) { 50 | return; 51 | } 52 | if (this.isFocused) { 53 | this.colonRef!.instance.classList.add("inverted"); 54 | } else { 55 | this.colonRef!.instance.classList.remove("inverted"); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Tri0Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {Degrees, Knots} from "../../data/Units"; 6 | import {SpeedFieldset} from "../../controls/selects/SpeedFieldset"; 7 | import {BearingFieldset} from "../../controls/selects/BearingFieldset"; 8 | 9 | 10 | type Tri0PageTypes = { 11 | tas: SpeedFieldset, 12 | windDir: BearingFieldset, 13 | windSpeed: SpeedFieldset, 14 | } 15 | 16 | /** 17 | * 5-2 18 | * The empty page can be seen here: https://www.youtube.com/shorts/9We5fcd2-VE 19 | */ 20 | export class Tri0Page extends SixLineHalfPage { 21 | 22 | public readonly cursorController; 23 | readonly children: UIElementChildren; 24 | 25 | readonly name: string = "TRI 0"; 26 | 27 | constructor(props: PageProps) { 28 | super(props); 29 | 30 | this.children = new UIElementChildren({ 31 | tas: new SpeedFieldset(this.props.memory.triPage.tas, this.setSpeed.bind(this)), 32 | windDir: new BearingFieldset(this.props.memory.triPage.windDirTrue, this.setWindDir.bind(this)), 33 | windSpeed: new SpeedFieldset(this.props.memory.triPage.windSpeed, this.setWindSpeed.bind(this)), 34 | }); 35 | 36 | this.cursorController = new CursorController(this.children); 37 | } 38 | 39 | public render(): VNode { 40 | return (
41 |              TRIP PLAN
42 |  ESTIMATES
43 |
44 | TAS:  {this.children.get("tas").render()}kt
45 | WIND: {this.children.get("windDir").render()}°¥
46 |       {this.children.get("windSpeed").render()}kt 47 |
); 48 | } 49 | 50 | private setSpeed(speed: Knots) { 51 | this.props.memory.triPage.tas = speed; 52 | } 53 | 54 | private setWindDir(dir: Degrees) { 55 | this.props.memory.triPage.windDirTrue = dir; 56 | } 57 | 58 | private setWindSpeed(speed: Knots) { 59 | this.props.memory.triPage.windSpeed = speed; 60 | } 61 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Oth4Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {List} from "../../controls/List"; 6 | import {SimpleListItem} from "../../controls/ListItem"; 7 | import {RemarksChangedEvent} from "../../settings/RemarksManager"; 8 | 9 | 10 | type Oth4PageTypes = { 11 | rmksList: List 12 | } 13 | 14 | export class Oth4Page extends SixLineHalfPage { 15 | 16 | public readonly cursorController; 17 | readonly children: UIElementChildren; 18 | 19 | readonly name: string = "OTH 4"; 20 | 21 | constructor(props: PageProps) { 22 | super(props); 23 | 24 | const list = this.props.remarksManager.getAirportsWithRemarks().map(ident => new SimpleListItem({ 25 | bus: this.props.bus, 26 | value: ident, 27 | fulltext: ident, 28 | onDelete: this.deleteRemark.bind(this), 29 | })); 30 | 31 | this.children = new UIElementChildren({ 32 | rmksList: new List(UIElementChildren.forList(list)), 33 | }); 34 | this.cursorController = new CursorController(this.children); 35 | 36 | this.props.bus.getSubscriber().on("changed").whenChanged().handle(this.refreshList.bind(this)); 37 | } 38 | 39 | public render(): VNode { 40 | return (
41 |             APTS W/RMKS
42 | {this.children.get("rmksList").render()} 43 |
); 44 | } 45 | 46 | private refreshList() { 47 | const list = this.props.remarksManager.getAirportsWithRemarks().map(ident => new SimpleListItem({ 48 | bus: this.props.bus, 49 | value: ident, 50 | fulltext: ident, 51 | onDelete: this.deleteRemark.bind(this), 52 | })); 53 | 54 | this.children.get("rmksList").refresh(UIElementChildren.forList(list)); 55 | 56 | this.cursorController.refreshChildren(this.children); 57 | 58 | } 59 | 60 | private deleteRemark(ident: string) { 61 | this.props.remarksManager.deleteRemarks(ident); 62 | } 63 | 64 | 65 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Set9Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController, NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {VolumeFieldset} from "../../controls/selects/VolumeFieldset"; 6 | 7 | 8 | type Set9PageTypes = { 9 | volume: VolumeFieldset; 10 | } 11 | 12 | /** 13 | * 3-57 14 | */ 15 | export class Set9Page extends SixLineHalfPage { 16 | 17 | public readonly cursorController; 18 | readonly children: UIElementChildren; 19 | 20 | readonly name: string = "SET 9"; 21 | 22 | protected readonly ref: NodeReference = FSComponent.createRef(); 23 | 24 | constructor(props: PageProps) { 25 | super(props); 26 | 27 | 28 | const volume = this.props.userSettings.getSetting("altAlertVolume").get(); 29 | 30 | this.children = new UIElementChildren({ 31 | volume: new VolumeFieldset(volume, this.saveVolume.bind(this)), 32 | }); 33 | 34 | if (this.props.planeSettings.output.altitudeAlertEnabled) { 35 | this.cursorController = new CursorController(this.children); 36 | } else { 37 | this.cursorController = NO_CURSOR_CONTROLLER; 38 | } 39 | 40 | } 41 | 42 | public render(): VNode { 43 | if (this.props.planeSettings.output.altitudeAlertEnabled) { 44 | return (
45 |                 ALTITUDE
46 |   ALERT
47 |  VOLUME:
48 |
49 |     {this.children.get("volume").render()} 50 |
); 51 | } else { 52 | return (
53 |                 ALTITUDE
54 |   ALERT
55 |  VOLUME
56 |    OFF
57 |  FEATURE
58 |  DISABLED 59 |
); 60 | } 61 | } 62 | 63 | private saveVolume(volume: number): void { 64 | this.props.userSettings.getSetting("altAlertVolume").set(volume); 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/RadialEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 3 | import {NumberEditorField} from "./EditorField"; 4 | import {format} from "numerable"; 5 | import {TickController} from "../../TickController"; 6 | 7 | export class RadialEditor extends Editor { 8 | 9 | private dotRef: NodeReference = FSComponent.createRef(); 10 | 11 | constructor(bus: EventBus, value: number | null, enterCallback: (text: number) => void) { 12 | super(bus, [ 13 | NumberEditorField.createWithMinMax(0, 3), 14 | new NumberEditorField(), 15 | new NumberEditorField(), 16 | new NumberEditorField(), 17 | ], value, enterCallback); 18 | } 19 | 20 | public render(): VNode { 21 | return ( 22 | {this.editorFields[0].render()}{this.editorFields[1].render()}{this.editorFields[2].render()}.{this.editorFields[3].render()} 24 | ); 25 | } 26 | 27 | protected convertFromValue(radial: number): Rawvalue { 28 | const numberString = format(radial, "000.0"); 29 | return [ 30 | Number(numberString.substring(0, 1)), 31 | Number(numberString.substring(1, 2)), 32 | Number(numberString.substring(2, 3)), 33 | Number(numberString.substring(4, 5)), 34 | ]; 35 | } 36 | 37 | protected convertToValue(rawValue: Rawvalue): Promise { 38 | const newValue = Number(String(rawValue[0]) + String(rawValue[1]) + String(rawValue[2]) + "." + String(rawValue[3])); 39 | if (newValue >= 360) { 40 | return Promise.resolve(null); 41 | } 42 | 43 | return Promise.resolve(newValue); 44 | } 45 | 46 | tick(blink: boolean): void { 47 | super.tick(blink); 48 | if (!TickController.checkRef(this.dotRef)) { 49 | return; 50 | } 51 | if (this.isFocused) { 52 | this.dotRef!.instance.classList.add("inverted"); 53 | } else { 54 | this.dotRef!.instance.classList.remove("inverted"); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Set10Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {SelectField} from "../../controls/selects/SelectField"; 6 | import {Button} from "../../controls/Button"; 7 | 8 | 9 | /** 10 | * Every fictitious setting the real KLN does not have 11 | */ 12 | 13 | 14 | type Set10PageTypes = { 15 | fastGps: SelectField; 16 | enableGlow: SelectField; 17 | importFlightplan: Button; 18 | } 19 | 20 | export class Set10Page extends SixLineHalfPage { 21 | 22 | public readonly cursorController; 23 | readonly children: UIElementChildren; 24 | 25 | readonly name: string = "SET10"; 26 | 27 | constructor(props: PageProps, setPage: (page: SixLineHalfPage) => void) { 28 | super(props); 29 | 30 | const fastGpsAcquisition = this.props.userSettings.getSetting("fastGpsAcquisition").get(); 31 | const glow = this.props.userSettings.getSetting("enableGlow").get(); 32 | 33 | this.children = new UIElementChildren({ 34 | fastGps: new SelectField(['REAL', 'FAST'], fastGpsAcquisition ? 1 : 0, this.saveFastGpsAcquisition.bind(this)), 35 | enableGlow: new SelectField(['OFF', ' ON'], glow ? 1 : 0, this.saveGlow.bind(this)), 36 | }); 37 | 38 | this.cursorController = new CursorController(this.children); 39 | } 40 | 41 | public render(): VNode { 42 | return (
43 |                 GPS:   {this.children.get("fastGps").render()}
44 | GLOW:   {this.children.get("enableGlow").render()}
45 |
); 46 | } 47 | 48 | private saveFastGpsAcquisition(fastGpsAcquisition: number): void { 49 | this.props.userSettings.getSetting("fastGpsAcquisition").set(fastGpsAcquisition === 1); 50 | if (fastGpsAcquisition === 1) { 51 | this.props.sensors.in.gps.gpsSatComputer.acquireAndUseSatellites(); 52 | } 53 | } 54 | 55 | private saveGlow(glowEnabled: number): void { 56 | this.props.userSettings.getSetting("enableGlow").set(glowEnabled === 1); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /kln90b/controls/selects/BearingFieldset.tsx: -------------------------------------------------------------------------------- 1 | import {SelectField} from "./SelectField"; 2 | import {FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {UiElement, UIElementChildren} from "../../pages/Page"; 4 | import {format} from "numerable"; 5 | import {Degrees} from "../../data/Units"; 6 | 7 | 8 | type BearingFieldsetTypes = { 9 | bearing10: SelectField; 10 | bearing1: SelectField; 11 | } 12 | 13 | const BEARING_10_SET = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35"]; 14 | const BEARING_1_SET = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; 15 | 16 | export class BearingFieldset implements UiElement { 17 | 18 | 19 | readonly children: UIElementChildren; 20 | 21 | constructor(private bearing: Degrees, private readonly callback: (bearing: Degrees) => void) { 22 | const bearingString = format(bearing, "000"); 23 | this.children = new UIElementChildren({ 24 | bearing10: new SelectField(BEARING_10_SET, Number(bearingString.substring(0, 2)), this.saveBearing10.bind(this)), 25 | bearing1: new SelectField(BEARING_1_SET, Number(bearingString.substring(2, 3)), this.saveBearing1.bind(this)), 26 | }); 27 | } 28 | 29 | render(): VNode { 30 | return ( 31 | {this.children.get("bearing10").render()}{this.children.get("bearing1").render()}); 32 | } 33 | 34 | tick(blink: boolean): void { 35 | } 36 | 37 | public setReadonly(readonly: boolean): void { 38 | this.children.get("bearing10").isReadonly = readonly; 39 | this.children.get("bearing1").isReadonly = readonly; 40 | } 41 | 42 | private saveBearing10(newBearing10: number): void { 43 | const oldBearing = format(this.bearing, "000"); 44 | this.bearing = Number(newBearing10 + oldBearing.substring(2)); 45 | this.callback(this.bearing); 46 | } 47 | 48 | private saveBearing1(newBearing1: number): void { 49 | const oldBearing = format(this.bearing, "000"); 50 | this.bearing = Number(oldBearing.substring(0, 2) + newBearing1); 51 | this.callback(this.bearing); 52 | } 53 | } -------------------------------------------------------------------------------- /kln90b/controls/editors/VorFreqEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 3 | import {NumberEditorField} from "./EditorField"; 4 | import {format} from "numerable"; 5 | import {TickController} from "../../TickController"; 6 | 7 | export class VorFreqEditor extends Editor { 8 | 9 | private dotRef: NodeReference = FSComponent.createRef(); 10 | 11 | constructor(bus: EventBus, value: number | null, enterCallback: (text: number) => void) { 12 | super(bus, [ 13 | NumberEditorField.createWithMinMax(1, 1), 14 | NumberEditorField.createWithMinMax(0, 1), 15 | new NumberEditorField(), 16 | new NumberEditorField(), 17 | new NumberEditorField(), 18 | ], value, enterCallback); 19 | } 20 | 21 | public render(): VNode { 22 | return ( 23 | {this.editorFields[0].render()}{this.editorFields[1].render()}{this.editorFields[2].render()}.{this.editorFields[3].render()}{this.editorFields[4].render()} 25 | ); 26 | } 27 | 28 | protected convertFromValue(value: number): Rawvalue { 29 | const numberString = format(value, "000.00"); 30 | 31 | return [ 32 | 0, 33 | Number(numberString.substring(1, 2)), 34 | Number(numberString.substring(2, 3)), 35 | Number(numberString.substring(4, 5)), 36 | Number(numberString.substring(5, 6)), 37 | ]; 38 | } 39 | 40 | protected convertToValue(rawValue: Rawvalue): Promise { 41 | const newValue = Number("1" + rawValue[1] + rawValue[2] + "." + rawValue[3] + rawValue[4]); 42 | if (newValue < 108 || newValue > 117.95) { 43 | return Promise.resolve(null); 44 | } 45 | return Promise.resolve(newValue); 46 | } 47 | 48 | tick(blink: boolean): void { 49 | super.tick(blink); 50 | if (!TickController.checkRef(this.dotRef)) { 51 | return; 52 | } 53 | if (this.isFocused) { 54 | this.dotRef!.instance.classList.add("inverted"); 55 | } else { 56 | this.dotRef!.instance.classList.remove("inverted"); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /kln90b/LVars.ts: -------------------------------------------------------------------------------- 1 | //############## 2 | //###Readonly### 3 | //############## 4 | export const LVAR_POWER = "L:KLN90B_Power"; // Boolean, position of the power switch. The unit itelf will still be off if electricity is not avaiable. Use H:KLN90B_Power_Toggle to set 5 | export const LVAR_RIGHT_SCAN = "L:KLN90B_RightScan"; //Boolean. false = in, normal. true = out, scan. Use H:KLN90B_RightScan_Toggle to set 6 | 7 | // Do not use GPS WP BEARING to animate a RMI, as that is optimized for the autopilot. This one will display the bearing according to appendix A of the manual. 8 | // Please see https://github.com/falcon71/kln90b/wiki/RMI for details 9 | export const LVAR_GPS_WP_BEARING = "L:KLN90B_GPS_WP_BEARING"; 10 | 11 | export const LVAR_HSI_TF_FLAGS = "L:KLN90B_HSI_TF_FLAGS"; //To from to animate the HSI. Please see https://github.com/falcon71/kln90b/wiki/CDI--HSI for details 12 | 13 | export const LVAR_ROLL_COMMAND = "L:KLN90B_RollCommand"; //Roll steering command for the autopilot. Please see https://github.com/falcon71/kln90b/wiki/Autopilot for details 14 | 15 | //External annunciators, see https://github.com/falcon71/kln90b/wiki/External-Annunciators for details 16 | export const LVAR_MSG_LIGHT = "L:KLN90B_MsgLight"; //Boolean. true whenever the MSG light flashes 17 | export const LVAR_WPT_LIGHT = "L:KLN90B_WptLight"; //Boolean. true if the waypoint alert is active 18 | export const LVAR_ANNUN_TEST = "L:KLN90B_AnnunTest"; //Boolean. true if the self-test is shown and all external annunciator lights should light up 19 | 20 | //############## 21 | //###Writable### 22 | //############## 23 | export const LVAR_BRIGHTNESS = "L:KLN90B_Brightness"; // Float, 0 to 1. This SimVar is writable for hardware, though the events H:KLN90B_Brt_Inc and H:KLN90B_Brt_Dec are preferred 24 | export const LVAR_DISABLE = "L:KLN90B_Disabled"; // Set to 1 to disable this device completely for hot swapping. See https://github.com/falcon71/kln90b/wiki/Hot-Swapping-and-Package-Detection for details 25 | export const LVAR_OBS_SOURCE = "L:KLN90B_ObsSource"; // Changes Input.ObsSource from the panel.xml on the fly 26 | export const LVAR_ELECTRICITY_INDEX = "L:KLN90B_ElectricitySimVarIndex"; // Changes the index of Input.ElectricitySimVar from the panel.xml on the fly 27 | export const LVAR_OBS_TARGET = "L:KLN90B_ObsTarget"; // Changes Output.ObsTarget from the panel.xml on the fly 28 | export const LVAR_GPS_SIMVARS = "L:KLN90B_WriteGpsSimvars"; // Changes Output.WriteGPSSimVars from the panel.xml on the fly 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /kln90b/controls/editors/MagvarEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 3 | import {EastWestEditorField, NumberEditorField} from "./EditorField"; 4 | import {format} from "numerable"; 5 | import {TickController} from "../../TickController"; 6 | 7 | export class MagvarEditor extends Editor { 8 | 9 | private degreeRef: NodeReference = FSComponent.createRef(); 10 | 11 | constructor(bus: EventBus, value: number | null, enterCallback: (text: number) => void) { 12 | super(bus, [ 13 | NumberEditorField.createWithBlankMax(9), 14 | new NumberEditorField(), 15 | new EastWestEditorField(), 16 | ], value, enterCallback); 17 | } 18 | 19 | public render(): VNode { 20 | return ( 21 | {this.editorFields[0].render()}{this.editorFields[1].render()}°{this.editorFields[2].render()} 23 | ); 24 | } 25 | 26 | protected convertFromValue(magvar: number): Rawvalue { 27 | let numberString; 28 | let eastWestIndex; 29 | 30 | if (magvar >= 180) { 31 | numberString = format(360 - magvar, "00"); 32 | eastWestIndex = 1; 33 | } else if (magvar < 0) { 34 | numberString = format(Math.abs(magvar), "00"); 35 | eastWestIndex = 1; 36 | } else { 37 | numberString = format(magvar, "00"); 38 | eastWestIndex = 0; 39 | } 40 | 41 | return [ 42 | Number(numberString.substring(0, 1)), 43 | Number(numberString.substring(1, 2)), 44 | eastWestIndex, 45 | ]; 46 | } 47 | 48 | protected convertToValue(rawValue: Rawvalue): Promise { 49 | const eastWestFactor = rawValue[2] == 0 ? 1 : -1; 50 | const newValue = Number(String(rawValue[0]) + String(rawValue[1])) * eastWestFactor; 51 | 52 | return Promise.resolve(newValue); 53 | } 54 | 55 | tick(blink: boolean): void { 56 | super.tick(blink); 57 | if (!TickController.checkRef(this.degreeRef)) { 58 | return; 59 | } 60 | if (this.isFocused) { 61 | this.degreeRef!.instance.classList.add("inverted"); 62 | } else { 63 | this.degreeRef!.instance.classList.remove("inverted"); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /kln90b/pages/right/Dt1Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {Dt1FplPage} from "./Dt1FplPage"; 6 | import {Dt1OtherPage} from "./Dt1OtherPage"; 7 | import {MainPage} from "../MainPage"; 8 | import {FplPage} from "../left/FplPage"; 9 | 10 | 11 | /** 12 | * 4-12 13 | */ 14 | export class Dt1Page extends SixLineHalfPage { 15 | 16 | public cursorController = NO_CURSOR_CONTROLLER; 17 | public children; 18 | 19 | readonly name: string = "D/T 1"; 20 | 21 | private readonly ref: NodeReference = FSComponent.createRef(); 22 | 23 | 24 | private readonly fplPage: Dt1FplPage; 25 | private readonly otherPage: Dt1OtherPage; 26 | 27 | private displayPage: Dt1FplPage | Dt1OtherPage; 28 | 29 | constructor(props: PageProps) { 30 | super(props); 31 | 32 | 33 | this.fplPage = new Dt1FplPage(props); 34 | this.otherPage = new Dt1OtherPage(props); 35 | 36 | const mainPage = this.props.pageManager.getCurrentPage() as MainPage; 37 | const page = mainPage.getLeftPage(); 38 | 39 | this.displayPage = page instanceof FplPage ? this.fplPage : this.otherPage; 40 | this.children = this.displayPage.children; 41 | this.cursorController = this.displayPage.cursorController; 42 | } 43 | 44 | public render(): VNode { 45 | return ( 46 |
47 | {this.displayPage.render()} 48 |
49 | ); 50 | } 51 | 52 | tick(blink: boolean) { 53 | const mainPage = this.props.pageManager.getCurrentPage() as MainPage; 54 | const page = mainPage.getLeftPage(); 55 | this.setPage(page instanceof FplPage ? this.fplPage : this.otherPage); 56 | 57 | super.tick(blink); 58 | this.displayPage.tick(blink); 59 | } 60 | 61 | protected redraw(): void { 62 | this.ref.instance.innerHTML = ""; 63 | FSComponent.render(this.displayPage.render(), this.ref.instance); 64 | } 65 | 66 | private setPage(newPage: Dt1FplPage | Dt1OtherPage) { 67 | if (this.displayPage !== newPage) { 68 | this.displayPage = newPage; 69 | this.children = newPage.children; 70 | this.cursorController = newPage.cursorController; 71 | this.requiresRedraw = true; 72 | } 73 | } 74 | 75 | 76 | } -------------------------------------------------------------------------------- /kln90b/pages/right/Dt2Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {Dt2FplPage} from "./Dt2FplPage"; 6 | import {Dt2OtherPage} from "./Dt2OtherPage"; 7 | import {MainPage} from "../MainPage"; 8 | import {FplPage} from "../left/FplPage"; 9 | 10 | 11 | /** 12 | * 4-12 13 | */ 14 | export class Dt2Page extends SixLineHalfPage { 15 | 16 | public cursorController = NO_CURSOR_CONTROLLER; 17 | public children; 18 | 19 | readonly name: string = "D/T 2"; 20 | 21 | private readonly ref: NodeReference = FSComponent.createRef(); 22 | 23 | 24 | private readonly fplPage: Dt2FplPage; 25 | private readonly otherPage: Dt2OtherPage; 26 | 27 | private displayPage: Dt2FplPage | Dt2OtherPage; 28 | 29 | constructor(props: PageProps) { 30 | super(props); 31 | 32 | 33 | this.fplPage = new Dt2FplPage(props); 34 | this.otherPage = new Dt2OtherPage(props); 35 | 36 | const mainPage = this.props.pageManager.getCurrentPage() as MainPage; 37 | const page = mainPage.getLeftPage(); 38 | 39 | this.displayPage = page instanceof FplPage ? this.fplPage : this.otherPage; 40 | this.children = this.displayPage.children; 41 | this.cursorController = this.displayPage.cursorController; 42 | } 43 | 44 | public render(): VNode { 45 | return ( 46 |
47 | {this.displayPage.render()} 48 |
49 | ); 50 | } 51 | 52 | tick(blink: boolean) { 53 | const mainPage = this.props.pageManager.getCurrentPage() as MainPage; 54 | const page = mainPage.getLeftPage(); 55 | this.setPage(page instanceof FplPage ? this.fplPage : this.otherPage); 56 | 57 | super.tick(blink); 58 | this.displayPage.tick(blink); 59 | } 60 | 61 | protected redraw(): void { 62 | this.ref.instance.innerHTML = ""; 63 | FSComponent.render(this.displayPage.render(), this.ref.instance); 64 | } 65 | 66 | private setPage(newPage: Dt2FplPage | Dt2OtherPage) { 67 | if (this.displayPage !== newPage) { 68 | this.displayPage = newPage; 69 | this.children = newPage.children; 70 | this.cursorController = newPage.cursorController; 71 | this.requiresRedraw = true; 72 | } 73 | } 74 | 75 | 76 | } -------------------------------------------------------------------------------- /kln90b/pages/right/Dt3Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {Dt3FplPage} from "./Dt3FplPage"; 6 | import {Dt3OtherPage} from "./Dt3OtherPage"; 7 | import {MainPage} from "../MainPage"; 8 | import {FplPage} from "../left/FplPage"; 9 | 10 | 11 | /** 12 | * 4-12 13 | */ 14 | export class Dt3Page extends SixLineHalfPage { 15 | 16 | public cursorController = NO_CURSOR_CONTROLLER; 17 | public children; 18 | 19 | readonly name: string = "D/T 3"; 20 | 21 | private readonly ref: NodeReference = FSComponent.createRef(); 22 | 23 | 24 | private readonly fplPage: Dt3FplPage; 25 | private readonly otherPage: Dt3OtherPage; 26 | 27 | private displayPage: Dt3FplPage | Dt3OtherPage; 28 | 29 | constructor(props: PageProps) { 30 | super(props); 31 | 32 | 33 | this.fplPage = new Dt3FplPage(props); 34 | this.otherPage = new Dt3OtherPage(props); 35 | 36 | const mainPage = this.props.pageManager.getCurrentPage() as MainPage; 37 | const page = mainPage.getLeftPage(); 38 | 39 | this.displayPage = page instanceof FplPage ? this.fplPage : this.otherPage; 40 | this.children = this.displayPage.children; 41 | this.cursorController = this.displayPage.cursorController; 42 | } 43 | 44 | public render(): VNode { 45 | return ( 46 |
47 | {this.displayPage.render()} 48 |
49 | ); 50 | } 51 | 52 | tick(blink: boolean) { 53 | const mainPage = this.props.pageManager.getCurrentPage() as MainPage; 54 | const page = mainPage.getLeftPage(); 55 | this.setPage(page instanceof FplPage ? this.fplPage : this.otherPage); 56 | 57 | super.tick(blink); 58 | this.displayPage.tick(blink); 59 | } 60 | 61 | protected redraw(): void { 62 | this.ref.instance.innerHTML = ""; 63 | FSComponent.render(this.displayPage.render(), this.ref.instance); 64 | } 65 | 66 | private setPage(newPage: Dt3FplPage | Dt3OtherPage) { 67 | if (this.displayPage !== newPage) { 68 | this.displayPage = newPage; 69 | this.children = newPage.children; 70 | this.cursorController = newPage.cursorController; 71 | this.requiresRedraw = true; 72 | } 73 | } 74 | 75 | 76 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/TempFieldset.tsx: -------------------------------------------------------------------------------- 1 | import {SelectField} from "./SelectField"; 2 | import {FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {UiElement, UIElementChildren} from "../../pages/Page"; 4 | import {format} from "numerable"; 5 | import {Celsius} from "../../data/Units"; 6 | 7 | 8 | type TempFieldsetTypes = { 9 | TempSign: SelectField; 10 | Temp10: SelectField; 11 | Temp1: SelectField; 12 | } 13 | 14 | const SIGN_SET = ["-", "0"]; 15 | const TEMP_SET = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; 16 | 17 | export class TempFieldset implements UiElement { 18 | 19 | 20 | readonly children: UIElementChildren; 21 | 22 | constructor(private temp: Celsius, private readonly callback: (temp: Celsius) => void) { 23 | const tempString = format(Math.abs(temp), "00"); 24 | this.children = new UIElementChildren({ 25 | TempSign: new SelectField(SIGN_SET, temp >= 0 ? 1 : 0, this.saveSign.bind(this)), 26 | Temp10: new SelectField(TEMP_SET, Number(tempString.substring(0, 1)), this.saveTemp10.bind(this)), 27 | Temp1: new SelectField(TEMP_SET, Number(tempString.substring(1, 2)), this.saveTemp1.bind(this)), 28 | }); 29 | } 30 | 31 | render(): VNode { 32 | return ( 33 | {this.children.get("TempSign").render()}{this.children.get("Temp10").render()}{this.children.get("Temp1").render()}); 34 | } 35 | 36 | tick(blink: boolean): void { 37 | } 38 | 39 | public setTemp(temp: Celsius): void { 40 | this.temp = temp; 41 | const tempString = format(Math.abs(temp), "00"); 42 | this.children.get("TempSign").value = temp >= 0 ? 1 : 0; 43 | this.children.get("Temp10").value = Number(tempString.substring(0, 1)); 44 | this.children.get("Temp1").value = Number(tempString.substring(1, 2)); 45 | 46 | } 47 | 48 | private saveSign(newSign: number): void { 49 | const oldTemp = Math.abs(this.temp); 50 | this.temp = oldTemp * (newSign === 0 ? -1 : 1); 51 | this.callback(this.temp); 52 | } 53 | 54 | private saveTemp10(newTemp10: number): void { 55 | const oldTemp = format(this.temp, "+00", {zeroFormat: "+00"}); 56 | this.temp = Number(oldTemp.substring(0, 1) + newTemp10 + oldTemp.substring(2)); 57 | this.callback(this.temp); 58 | } 59 | 60 | private saveTemp1(newTemp1: number): void { 61 | const oldTemp = format(this.temp, "+00", {zeroFormat: "+00"}); 62 | this.temp = Number(oldTemp.substring(0, 2) + newTemp1); 63 | this.callback(this.temp); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /kln90b/controls/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import {ComponentProps, DisplayComponent, EventBus, FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {VERSION} from "../Version"; 3 | 4 | 5 | export interface ErrorEvent { 6 | error: Error; 7 | } 8 | 9 | export interface ErrorPageProps extends ComponentProps { 10 | bus: EventBus; 11 | } 12 | 13 | /** 14 | * This class is needed, so the keyboard is always available and retains its state if the active page changes 15 | */ 16 | export class ErrorPage extends DisplayComponent { 17 | 18 | private containerRef: NodeReference = FSComponent.createRef(); 19 | private errorRef: NodeReference = FSComponent.createRef(); 20 | private okRef: NodeReference = FSComponent.createRef(); 21 | private okSuppressRef: NodeReference = FSComponent.createRef(); 22 | 23 | private showErrors: boolean = true; 24 | 25 | 26 | constructor(props: ErrorPageProps) { 27 | super(props); 28 | 29 | props.bus.getSubscriber().on("error").handle(this.showError.bind(this)); 30 | } 31 | 32 | 33 | render(): VNode { 34 | return (
35 |
An error occured. Please report at https://github.com/falcon71/kln90b or on 36 | Discord: 37 |
38 |
39 |
40 | 41 |
{VERSION}
42 | 44 |
45 |
); 46 | } 47 | 48 | public showError(error: Error) { 49 | console.error(error); 50 | if (this.showErrors) { 51 | this.errorRef.instance.innerHTML = error.toString() + "
" + error.stack; 52 | this.containerRef.instance.classList.remove("d-none") 53 | } 54 | } 55 | 56 | public onAfterRender(thisNode: VNode): void { 57 | this.okRef.instance.onclick = this.ok.bind(this); 58 | this.okSuppressRef.instance.onclick = this.okAndSurpress.bind(this); 59 | } 60 | 61 | public ok() { 62 | this.containerRef.instance.classList.add("d-none") 63 | } 64 | 65 | public okAndSurpress() { 66 | this.showErrors = false; 67 | this.containerRef.instance.classList.add("d-none") 68 | } 69 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/SpeedFieldset.tsx: -------------------------------------------------------------------------------- 1 | import {SelectField} from "./SelectField"; 2 | import {FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {UiElement, UIElementChildren} from "../../pages/Page"; 4 | import {format} from "numerable"; 5 | import {Knots} from "../../data/Units"; 6 | 7 | 8 | type SpeedFieldsetTypes = { 9 | Speed100: SelectField; 10 | Speed10: SelectField; 11 | Speed1: SelectField; 12 | } 13 | 14 | const SPEED_SET = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; 15 | 16 | export class SpeedFieldset implements UiElement { 17 | 18 | 19 | readonly children: UIElementChildren; 20 | 21 | constructor(private speed: Knots, private readonly callback: (Speed: Knots) => void) { 22 | const SpeedString = format(speed, "000"); 23 | this.children = new UIElementChildren({ 24 | Speed100: new SelectField(SPEED_SET, Number(SpeedString.substring(0, 1)), this.saveSpeed100.bind(this)), 25 | Speed10: new SelectField(SPEED_SET, Number(SpeedString.substring(1, 2)), this.saveSpeed10.bind(this)), 26 | Speed1: new SelectField(SPEED_SET, Number(SpeedString.substring(2, 3)), this.saveSpeed1.bind(this)), 27 | }); 28 | } 29 | 30 | render(): VNode { 31 | return ( 32 | {this.children.get("Speed100").render()}{this.children.get("Speed10").render()}{this.children.get("Speed1").render()}); 33 | } 34 | 35 | tick(blink: boolean): void { 36 | } 37 | 38 | public setSpeed(speed: Knots): void { 39 | this.speed = speed; 40 | const SpeedString = format(speed, "000"); 41 | this.children.get("Speed100").value = Number(SpeedString.substring(0, 1)); 42 | this.children.get("Speed10").value = Number(SpeedString.substring(1, 2)); 43 | this.children.get("Speed1").value = Number(SpeedString.substring(2, 3)); 44 | } 45 | 46 | private saveSpeed100(newSpeed100: number): void { 47 | const oldSpeed = format(this.speed, "000"); 48 | this.speed = Number(newSpeed100 + oldSpeed.substring(1)); 49 | this.callback(this.speed); 50 | } 51 | 52 | private saveSpeed10(newSpeed10: number): void { 53 | const oldSpeed = format(this.speed, "000"); 54 | this.speed = Number(oldSpeed.substring(0, 1) + newSpeed10 + oldSpeed.substring(2)); 55 | this.callback(this.speed); 56 | } 57 | 58 | private saveSpeed1(newSpeed1: number): void { 59 | const oldSpeed = format(this.speed, "000"); 60 | this.speed = Number(oldSpeed.substring(0, 2) + newSpeed1); 61 | this.callback(this.speed); 62 | } 63 | 64 | 65 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Oth10Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {TemperatureDisplay} from "../../controls/displays/TemperatureDisplay"; 6 | import {AltitudeDisplay} from "../../controls/displays/AltitudeDisplay"; 7 | import {pressureAlt2DensityAlt} from "../../data/Conversions"; 8 | 9 | 10 | type Oth10PageTypes = { 11 | sat: TemperatureDisplay, 12 | tat: TemperatureDisplay, 13 | pressureAlt: AltitudeDisplay, 14 | densityAlt: AltitudeDisplay, 15 | } 16 | 17 | /** 18 | * 5-43 19 | */ 20 | export class Oth10Page extends SixLineHalfPage { 21 | 22 | public readonly cursorController = NO_CURSOR_CONTROLLER; 23 | readonly children: UIElementChildren; 24 | 25 | readonly name: string; 26 | 27 | 28 | constructor(props: PageProps) { 29 | super(props); 30 | 31 | this.name = this.props.planeSettings.input.fuelComputer.isInterfaced ? "OTH10" : "OTH 6"; 32 | 33 | this.children = new UIElementChildren({ 34 | sat: new TemperatureDisplay(this.props.sensors.in.airdata.sat), 35 | tat: new TemperatureDisplay(this.props.sensors.in.airdata.tat), 36 | pressureAlt: new AltitudeDisplay(this.props.sensors.in.airdata.pressureAltitude), 37 | densityAlt: new AltitudeDisplay(null), 38 | }); 39 | } 40 | 41 | public render(): VNode { 42 | return (
43 |                  AIR DATA
44 |
45 | SAT   {this.children.get("sat").render()}
46 | TAT   {this.children.get("tat").render()}
47 | PRS {this.children.get("pressureAlt").render()}ft
48 | DEN {this.children.get("densityAlt").render()}ft 49 |
); 50 | } 51 | 52 | public tick(blink: boolean): void { 53 | this.requiresRedraw = true; 54 | super.tick(blink); 55 | } 56 | 57 | protected redraw(): void { 58 | this.children.get("sat").temperature = this.props.sensors.in.airdata.sat; 59 | this.children.get("tat").temperature = this.props.sensors.in.airdata.tat; 60 | this.children.get("pressureAlt").altitude = this.props.sensors.in.airdata.pressureAltitude; 61 | this.children.get("densityAlt").altitude = pressureAlt2DensityAlt(this.props.sensors.in.airdata.pressureAltitude!, this.props.sensors.in.airdata.sat); //normally transmitted directly by the airdata system... 62 | } 63 | } -------------------------------------------------------------------------------- /kln90b/settings/UserFlightplanPersistor.ts: -------------------------------------------------------------------------------- 1 | import {DefaultUserSettingManager, EventBus, FacilityClient, ICAO} from "@microsoft/msfs-sdk"; 2 | import {KLN90BUserFlightplansSettings, KLN90BUserFlightplansTypes} from "./KLN90BUserFlightplans"; 3 | import {Flightplan, FlightplanEvents, KLNFlightplanLeg, KLNLegType} from "../data/flightplan/Flightplan"; 4 | import {MessageHandler} from "../data/MessageHandler"; 5 | import {Flightplanloader} from "../services/Flightplanloader"; 6 | import {KLN90BUserSettings} from "./KLN90BUserSettings"; 7 | import {UserFlightplanLoaderV2} from "./UserFlightplanLoaderV2"; 8 | import {UserFlightplanLoaderV1} from "./UserFlightplanLoaderV1"; 9 | 10 | 11 | export interface UserFlightplanLoader { 12 | restoreAllFlightplan(): Promise; 13 | } 14 | 15 | /** 16 | * In the real unit, FPL 0 is also persisted: https://youtu.be/S1lt2W95bLA?t=181 17 | */ 18 | export class UserFlightplanPersistor extends Flightplanloader { 19 | private manager: DefaultUserSettingManager; 20 | 21 | private v1Loader: UserFlightplanLoader; 22 | private v2Loader: UserFlightplanLoader; 23 | 24 | 25 | constructor(bus: EventBus, facilityLoader: FacilityClient, messageHandler: MessageHandler, private readonly userSettings: KLN90BUserSettings) { 26 | super(bus, facilityLoader, messageHandler); 27 | bus.getSubscriber().on("flightplanChanged").handle(this.persistFlightplan.bind(this)); 28 | this.manager = KLN90BUserFlightplansSettings.getManager(bus); 29 | this.v1Loader = new UserFlightplanLoaderV1(bus, facilityLoader, messageHandler); 30 | this.v2Loader = new UserFlightplanLoaderV2(bus, facilityLoader, messageHandler); 31 | } 32 | 33 | public restoreAllFlightplan(): Promise { 34 | if (this.userSettings.getSetting("userDataFormat").get() === 2) { 35 | return this.v2Loader.restoreAllFlightplan(); 36 | } else { 37 | return this.v1Loader.restoreAllFlightplan(); 38 | } 39 | 40 | 41 | } 42 | 43 | public persistFlightplan(fpl: Flightplan) { 44 | const setting = this.manager.getSetting(`fpl${fpl.idx}`); 45 | 46 | let serialized = ""; 47 | for (const leg of fpl.getLegs()) { 48 | if (leg.type === KLNLegType.USER) { 49 | serialized += this.serializeLeg(leg); 50 | } 51 | } 52 | console.log("persisting flightplan", fpl, serialized); 53 | setting.set(serialized); 54 | } 55 | 56 | private serializeLeg(leg: KLNFlightplanLeg): string { 57 | return ICAO.valueToStringV2(leg.wpt.icaoStruct); 58 | } 59 | 60 | 61 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/SelectField.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from '@microsoft/msfs-sdk'; 2 | import {EnterResult, Field} from "../../pages/CursorController"; 3 | import {NO_CHILDREN} from '../../pages/Page'; 4 | import {TickController} from "../../TickController"; 5 | 6 | 7 | export class SelectField implements Field { 8 | readonly children = NO_CHILDREN; 9 | public isEntered = false; 10 | public isFocused = false; 11 | public isReadonly = false; 12 | protected readonly ref: NodeReference = FSComponent.createRef(); 13 | 14 | public constructor(protected valueSet: string[], public value: number, protected readonly changedCallback: (value: number) => void) { 15 | } 16 | 17 | 18 | public render(): VNode { 19 | return ( 20 | {this.valueSet[this.value]}); 21 | } 22 | 23 | 24 | public setFocused(focused: boolean) { 25 | this.isFocused = focused; 26 | } 27 | 28 | 29 | outerLeft(): boolean { 30 | return false; 31 | } 32 | 33 | outerRight(): boolean { 34 | return false; 35 | } 36 | 37 | innerLeft(): boolean { 38 | this.value--; 39 | if (this.value < 0) { 40 | this.value = this.valueSet.length - 1; 41 | } 42 | this.changedCallback(this.value); 43 | return true; 44 | } 45 | 46 | innerRight(): boolean { 47 | this.value++; 48 | if (this.value >= this.valueSet.length) { 49 | this.value = 0; 50 | } 51 | this.changedCallback(this.value); 52 | return true; 53 | } 54 | 55 | 56 | isEnterAccepted(): boolean { 57 | return false; 58 | } 59 | 60 | enter(): Promise { 61 | return Promise.resolve(EnterResult.Not_Handled); 62 | } 63 | 64 | tick(blink: boolean): void { 65 | if (!TickController.checkRef(this.ref)) { 66 | return; 67 | } 68 | 69 | this.ref.instance.textContent = this.valueSet[this.value]; 70 | 71 | if (this.isFocused) { 72 | this.ref!.instance.classList.add("inverted"); 73 | } else { 74 | this.ref!.instance.classList.remove("inverted"); 75 | } 76 | } 77 | 78 | 79 | clear(): boolean { 80 | return false; 81 | } 82 | 83 | isClearAccepted(): boolean { 84 | return false; 85 | } 86 | 87 | public keyboard(key: string): boolean { 88 | const idx = this.valueSet.indexOf(key); 89 | if (idx == -1) { 90 | return false; 91 | } 92 | 93 | this.value = idx; 94 | this.changedCallback(this.value); 95 | return true; 96 | } 97 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Oth7Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {TextDisplay} from "../../controls/displays/TextDisplay"; 6 | import {FuelFlowDisplay} from "../../controls/displays/FuelDisplay"; 7 | 8 | 9 | type Oth7PageTypes = { 10 | fuelUnit: TextDisplay, 11 | ff1: FuelFlowDisplay, 12 | ff2: FuelFlowDisplay, 13 | fftotal: FuelFlowDisplay, 14 | } 15 | 16 | /** 17 | * 5-41 18 | */ 19 | export class Oth7Page extends SixLineHalfPage { 20 | 21 | public readonly cursorController = NO_CURSOR_CONTROLLER; 22 | readonly children: UIElementChildren; 23 | 24 | readonly name: string = "OTH 7"; 25 | 26 | 27 | constructor(props: PageProps) { 28 | super(props); 29 | 30 | this.children = new UIElementChildren({ 31 | fuelUnit: new TextDisplay(this.props.planeSettings.input.fuelComputer.unit.padStart(3, " ")), 32 | ff1: new FuelFlowDisplay(0), 33 | ff2: new FuelFlowDisplay(0), 34 | fftotal: new FuelFlowDisplay(0), 35 | }); 36 | } 37 | 38 | public render(): VNode { 39 | if (this.props.sensors.in.fuelComputer.numberOfEngines === 2) { 40 | return (
41 |                  FUEL FLOW
42 |
43 |      {this.children.get("fuelUnit").render()}/HR
44 | ENG 1  {this.children.get("ff1").render()}
45 | ENG 2  {this.children.get("ff2").render()}
46 | TOTAL  {this.children.get("fftotal").render()} 47 |
); 48 | } else { 49 | return (
50 |                  FUEL FLOW
51 |
52 |      {this.children.get("fuelUnit").render()}/HR
53 |
54 |
55 |        {this.children.get("fftotal").render()} 56 |
); 57 | } 58 | } 59 | 60 | public tick(blink: boolean): void { 61 | this.requiresRedraw = true; 62 | super.tick(blink); 63 | } 64 | 65 | protected redraw(): void { 66 | 67 | this.children.get("ff1").fuelFlow = this.props.sensors.in.fuelComputer.fuelFlow1; 68 | this.children.get("ff2").fuelFlow = this.props.sensors.in.fuelComputer.fuelFlow2; 69 | this.children.get("fftotal").fuelFlow = this.props.sensors.in.fuelComputer.fuelFlow1 + this.props.sensors.in.fuelComputer.fuelFlow2; 70 | } 71 | } -------------------------------------------------------------------------------- /cfg/panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | KLN90B 4 | false 5 | 6 | true 7 | 1 8 | false 9 | CIRCUIT ON:1 10 | 11 | false 12 | 0 13 | 14 | 15 | false 16 | GAL 17 | Avgas 18 | true 19 | true 20 | 21 | 22 | false 23 | false 24 | 25 | 26 | 27 | 0 28 | false 29 | true 30 | 31 | 32 | -------------------------------------------------------------------------------- /kln90b/pages/left/Set3Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {SelectField} from "../../controls/selects/SelectField"; 6 | import {SURFACE_HRD, SURFACE_HRD_SFT} from "../../settings/KLN90BUserSettings"; 7 | import {format} from "numerable"; 8 | 9 | 10 | type Set3PageTypes = { 11 | runwayLength: SelectField, 12 | runwaySurface: SelectField, 13 | } 14 | 15 | /** 16 | * 3-22 17 | */ 18 | export class Set3Page extends SixLineHalfPage { 19 | 20 | public readonly cursorController; 21 | readonly children: UIElementChildren; 22 | 23 | readonly name: string = "SET 3"; 24 | 25 | 26 | constructor(props: PageProps) { 27 | super(props); 28 | 29 | const lengthValueSet = []; 30 | for (let i = 1000; i <= 5000; i += 100) { 31 | lengthValueSet.push(format(i, "0000")); 32 | } 33 | const lengthIndex = (this.props.userSettings.getSetting("nearestAptMinRunwayLength").get() - 1000) / 100; 34 | const surfaceIndex = this.props.userSettings.getSetting("nearestAptSurface").get() === SURFACE_HRD_SFT ? 0 : 1; 35 | 36 | 37 | this.children = new UIElementChildren({ 38 | runwayLength: new SelectField(lengthValueSet, lengthIndex, this.setMinLength.bind(this)), 39 | runwaySurface: new SelectField([" HRD SFT", " SFT"], surfaceIndex, this.setSurface.bind(this)), 40 | }); 41 | 42 | this.cursorController = new CursorController(this.children); 43 | } 44 | 45 | public render(): VNode { 46 | return (
47 |             NEAREST APT
48 |  CRITERIA
49 | MIN LENGTH:
50 |       {this.children.get("runwayLength").render()}'
51 | SURFACE
52 | {this.children.get("runwaySurface").render()} 53 |
); 54 | } 55 | 56 | private setMinLength(lengthIndex: number) { 57 | const length = lengthIndex * 100 + 1000; 58 | 59 | this.props.userSettings.getSetting("nearestAptMinRunwayLength").set(length); 60 | 61 | // noinspection JSIgnoredPromiseFromCall 62 | this.props.nearestLists.aptNearestList.updateFilters(); 63 | } 64 | 65 | private setSurface(surfaceIndex: number) { 66 | this.props.userSettings.getSetting("nearestAptSurface").set(surfaceIndex === 0 ? SURFACE_HRD_SFT : SURFACE_HRD); 67 | 68 | // noinspection JSIgnoredPromiseFromCall 69 | this.props.nearestLists.aptNearestList.updateFilters(); 70 | } 71 | 72 | 73 | } -------------------------------------------------------------------------------- /kln90b/services/Flightplanloader.ts: -------------------------------------------------------------------------------- 1 | import {Flightplan, KLNFlightplanLeg, KLNLegType} from "../data/flightplan/Flightplan"; 2 | import {Message, MessageHandler, OneTimeMessage} from "../data/MessageHandler"; 3 | import {EventBus, FacilityClient, ICAO, IcaoValue} from "@microsoft/msfs-sdk"; 4 | import {buildIcaoStructIdentOnly} from "../data/navdata/IcaoBuilder"; 5 | 6 | export abstract class Flightplanloader { 7 | protected constructor(protected readonly bus: EventBus, protected readonly facilityLoader: FacilityClient, protected messageHandler: MessageHandler) { 8 | } 9 | 10 | /** 11 | * Converts the list of icaos into a KLN flightplan. It will keep the number of legs below 30 and it will ignore 12 | * wapoints that can no longer be found. Messages will be generated for each deleted waypoint 13 | * @param icaos 14 | * @param fplIdx 15 | * @protected 16 | */ 17 | protected async loadIcaos(icaos: IcaoValue[], fplIdx: number = 0): Promise { 18 | let messages: Message[] = []; 19 | 20 | for (let i = 30; i < icaos.length; i++) { //I'm terribly sorry, but the KLN90B flightplans can only have a maximum of 30 legs 21 | messages.push(new OneTimeMessage([`WAYPOINT ${icaos[i].ident} DELETED`])); 22 | } 23 | 24 | const promises = icaos.slice(0, 30).map(this.convertToKLNLeg.bind(this)); 25 | const legs = await Promise.all(promises); 26 | 27 | messages.push(...(legs.filter(l => l instanceof OneTimeMessage) as OneTimeMessage[])); 28 | if (messages.length > 10) { 29 | messages = messages.slice(0, 10); 30 | messages.push(new OneTimeMessage(["OTHER WAYPOINTS DELETED"])); 31 | } 32 | messages.forEach(this.messageHandler.addMessage.bind(this.messageHandler)); 33 | 34 | const fpl = new Flightplan(fplIdx, legs.filter(l => !(l instanceof OneTimeMessage)) as KLNFlightplanLeg[], this.bus); 35 | console.log("loaded flightplan", fpl); 36 | return fpl; 37 | } 38 | 39 | /** 40 | * This waypoint will not be found and cause a WAYPOINT DELETED message 41 | * @param ident 42 | * @protected 43 | */ 44 | protected notFoundIcao(ident: string): IcaoValue { 45 | return buildIcaoStructIdentOnly(ident); 46 | } 47 | 48 | private async convertToKLNLeg(icao: IcaoValue): Promise { 49 | try { 50 | const facility = await this.facilityLoader.getFacility(ICAO.getFacilityTypeFromValue(icao), icao); 51 | return {wpt: facility, type: KLNLegType.USER}; 52 | } catch (e) { 53 | console.error(`Error converting ${icao}`, e); 54 | return new OneTimeMessage([`WAYPOINT ${icao.ident} DELETED`]); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/TimeFieldset.tsx: -------------------------------------------------------------------------------- 1 | import {SelectField} from "./SelectField"; 2 | import {FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {UiElement, UIElementChildren} from "../../pages/Page"; 4 | import {format} from "numerable"; 5 | import {TimeStamp} from "../../data/Time"; 6 | 7 | 8 | type TimeFieldsetTypes = { 9 | hour: SelectField; 10 | minute10: SelectField; 11 | minute1: SelectField; 12 | } 13 | 14 | const HOUR_SET = Array(24).fill(null).map((_, idx) => format(idx, "00")); 15 | const MINUTE10_SET = ["0", "1", "2", "3", "4", "5"]; 16 | const MINUTE1_SET = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; 17 | 18 | export class TimeFieldset implements UiElement { 19 | 20 | 21 | readonly children: UIElementChildren; 22 | 23 | constructor(private time: TimeStamp, private readonly callback: (time: TimeStamp) => void) { 24 | const minuteString = format(time.getMinutes(), "00"); 25 | this.children = new UIElementChildren({ 26 | hour: new SelectField(HOUR_SET, time.getHours(), this.saveHours.bind(this)), 27 | minute10: new SelectField(MINUTE10_SET, Number(minuteString.substring(0, 1)), this.saveMinute10.bind(this)), 28 | minute1: new SelectField(MINUTE1_SET, Number(minuteString.substring(1, 2)), this.saveMinute1.bind(this)), 29 | }); 30 | } 31 | 32 | render(): VNode { 33 | return ( 34 | {this.children.get("hour").render()}:{this.children.get("minute10").render()}{this.children.get("minute1").render()}); 35 | } 36 | 37 | tick(blink: boolean): void { 38 | } 39 | 40 | public setTime(time: TimeStamp): void { 41 | this.time = time; 42 | const minuteString = format(time.getMinutes(), "00"); 43 | this.children.get("hour").value = time.getHours(); 44 | this.children.get("minute10").value = Number(minuteString.substring(0, 1)); 45 | this.children.get("minute1").value = Number(minuteString.substring(1, 2)); 46 | 47 | } 48 | 49 | private saveHours(newHours: number): void { 50 | this.time = this.time.withTime(newHours, this.time.getMinutes()); 51 | this.callback(this.time); 52 | } 53 | 54 | private saveMinute10(newMinutes10: number): void { 55 | const oldTime = format(this.time.getMinutes(), "00"); 56 | this.time = this.time.withTime(this.time.getHours(), Number(newMinutes10 + oldTime.substring(1))); 57 | this.callback(this.time); 58 | } 59 | 60 | private saveMinute1(newMinutes1: number): void { 61 | const oldTime = format(this.time.getMinutes(), "00"); 62 | this.time = this.time.withTime(this.time.getHours(), Number(oldTime.substring(0, 1) + newMinutes1)); 63 | this.callback(this.time); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Set8Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {SelectField} from "../../controls/selects/SelectField"; 6 | import {AltitudeFieldset} from "../../controls/selects/AltitudeFieldset"; 7 | 8 | 9 | type Set8PageTypes = { 10 | enable: SelectField; 11 | vertBuffer: AltitudeFieldset; 12 | } 13 | 14 | /** 15 | * 3-41 16 | */ 17 | export class Set8Page extends SixLineHalfPage { 18 | 19 | public readonly cursorController; 20 | readonly children: UIElementChildren; 21 | 22 | readonly name: string = "SET 8"; 23 | 24 | protected readonly ref: NodeReference = FSComponent.createRef(); 25 | 26 | constructor(props: PageProps) { 27 | super(props); 28 | 29 | 30 | const enabled = this.props.userSettings.getSetting("airspaceAlertEnabled").get(); 31 | 32 | this.children = new UIElementChildren({ 33 | enable: new SelectField(["DISABLE", " ENABLE"], enabled ? 1 : 0, this.saveEnableAirspaceAlert.bind(this)), 34 | vertBuffer: new AltitudeFieldset(this.props.userSettings.getSetting("airspaceAlertBuffer").get(), this.saveAirspaceBuffer.bind(this)), 35 | 36 | }); 37 | 38 | this.children.get("vertBuffer").setReadonly(!enabled); 39 | 40 | this.cursorController = new CursorController(this.children); 41 | } 42 | 43 | public render(): VNode { 44 | return (
45 |              AIRSPACE
46 |   ALERT
47 |  {this.children.get("enable").render()}
48 |
49 |
50 | VERT BUFFER
51 |    ±{this.children.get("vertBuffer").render()}ft 52 |
53 |
); 54 | } 55 | 56 | protected redraw() { 57 | super.redraw(); 58 | 59 | const enabled = this.props.userSettings.getSetting("airspaceAlertEnabled").get(); 60 | 61 | if (enabled) { 62 | this.ref.instance.classList.remove("d-none"); 63 | } else { 64 | this.ref.instance.classList.add("d-none"); 65 | } 66 | } 67 | 68 | private saveEnableAirspaceAlert(enabled: number): void { 69 | this.props.userSettings.getSetting("airspaceAlertEnabled").set(enabled === 1); 70 | 71 | this.children.get("vertBuffer").setReadonly(enabled === 0); 72 | this.requiresRedraw = true; 73 | } 74 | 75 | private saveAirspaceBuffer(buffer: number): void { 76 | this.props.userSettings.getSetting("airspaceAlertBuffer").set(buffer); 77 | } 78 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Oth8Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {NO_CURSOR_CONTROLLER} from "../CursorController"; 5 | import {TextDisplay} from "../../controls/displays/TextDisplay"; 6 | import {OthFuelDisplay} from "../../controls/displays/FuelDisplay"; 7 | 8 | 9 | type Oth8PageTypes = { 10 | fuelUnit: TextDisplay, 11 | used1: OthFuelDisplay, 12 | used2: OthFuelDisplay, 13 | usedTotal: OthFuelDisplay, 14 | } 15 | 16 | /** 17 | * 5-41 18 | */ 19 | export class Oth8Page extends SixLineHalfPage { 20 | 21 | public readonly cursorController = NO_CURSOR_CONTROLLER; 22 | readonly children: UIElementChildren; 23 | 24 | readonly name: string = "OTH 8"; 25 | 26 | 27 | constructor(props: PageProps) { 28 | super(props); 29 | 30 | this.children = new UIElementChildren({ 31 | fuelUnit: new TextDisplay(this.props.planeSettings.input.fuelComputer.unit.padStart(3, " ")), 32 | used1: new OthFuelDisplay(null), 33 | used2: new OthFuelDisplay(null), 34 | usedTotal: new OthFuelDisplay(null), 35 | }); 36 | } 37 | 38 | public render(): VNode { 39 | if (this.props.sensors.in.fuelComputer.numberOfEngines === 2) { 40 | return (
41 |                  FUEL USED
42 |
43 |         {this.children.get("fuelUnit").render()}
44 | ENG 1 {this.children.get("used1").render()}
45 | ENG 2 {this.children.get("used2").render()}
46 | TOTAL {this.children.get("usedTotal").render()} 47 |
); 48 | } else { 49 | return (
50 |                  FUEL USED
51 |
52 |         {this.children.get("fuelUnit").render()}
53 |
54 |
55 |       {this.children.get("usedTotal").render()} 56 |
); 57 | } 58 | } 59 | 60 | public tick(blink: boolean): void { 61 | if (this.props.planeSettings.input.fuelComputer.fuelUsedTransmitted) { 62 | this.requiresRedraw = true; 63 | } 64 | super.tick(blink); 65 | } 66 | 67 | protected redraw(): void { 68 | this.children.get("used1").fuel = this.props.sensors.in.fuelComputer.fuelUsed1; 69 | this.children.get("used2").fuel = this.props.sensors.in.fuelComputer.fuelUsed2; 70 | this.children.get("usedTotal").fuel = this.props.sensors.in.fuelComputer.fuelUsed1! + this.props.sensors.in.fuelComputer.fuelUsed2!; 71 | } 72 | } -------------------------------------------------------------------------------- /kln90b/services/AltAlert.ts: -------------------------------------------------------------------------------- 1 | import {CalcTickable} from "../TickController"; 2 | import {VolatileMemory} from "../data/VolatileMemory"; 3 | import {KLN90PlaneSettings} from "../settings/KLN90BPlaneSettings"; 4 | import {Sensors} from "../Sensors"; 5 | import {Feet} from "../data/Units"; 6 | 7 | enum AltAlertState { 8 | ARMED, 9 | REACHING, 10 | REACHED, 11 | } 12 | 13 | /** 14 | * 3-56 15 | */ 16 | export class AltAlert implements CalcTickable { 17 | 18 | private state: AltAlertState = AltAlertState.ARMED; 19 | private selectedAltitude = 0; 20 | 21 | 22 | constructor(private memory: VolatileMemory, private settings: KLN90PlaneSettings, private sensors: Sensors) { 23 | } 24 | 25 | public tick(): void { 26 | const indicatedAlt = this.sensors.in.airdata.getIndicatedAlt(); 27 | if (!this.memory.altPage.alertEnabled || indicatedAlt === null || !this.settings.output.altitudeAlertEnabled) { 28 | return; 29 | } 30 | 31 | const roundetAlt = Math.round(indicatedAlt / 100) * 100; 32 | if (this.memory.navPage.nav4SelectedAltitude !== this.selectedAltitude) { 33 | this.state = AltAlertState.ARMED; 34 | this.selectedAltitude = this.memory.navPage.nav4SelectedAltitude; 35 | } 36 | 37 | 38 | switch (this.state) { 39 | case AltAlertState.ARMED: 40 | if (roundetAlt === this.selectedAltitude) { 41 | this.state = AltAlertState.REACHED; 42 | this.sensors.out.audioGenerator.shortBeeps(2); 43 | } else if (this.isAltWithinWindow(roundetAlt, this.selectedAltitude, 1000)) { 44 | this.state = AltAlertState.REACHING; 45 | this.sensors.out.audioGenerator.shortBeeps(3); 46 | } 47 | return; 48 | case AltAlertState.REACHING: 49 | if (roundetAlt === this.selectedAltitude) { 50 | this.state = AltAlertState.REACHED; 51 | this.sensors.out.audioGenerator.shortBeeps(2); 52 | } else if (!this.isAltWithinWindow(roundetAlt, this.selectedAltitude, 1000)) { 53 | this.state = AltAlertState.ARMED; 54 | } 55 | return; 56 | case AltAlertState.REACHED: 57 | if (!this.isAltWithinWindow(roundetAlt, this.selectedAltitude, this.memory.altPage.alertWarn)) { 58 | this.state = AltAlertState.REACHING; 59 | this.sensors.out.audioGenerator.shortBeeps(4); 60 | } 61 | return; 62 | } 63 | 64 | } 65 | 66 | private isAltWithinWindow(indicatedAlt: Feet, selectedAlt: Feet, window: Feet) { 67 | return indicatedAlt >= selectedAlt - window && indicatedAlt <= selectedAlt + window; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /kln90b/services/MSA.ts: -------------------------------------------------------------------------------- 1 | import {Feet} from "../data/Units"; 2 | import {GeoPoint, LatLonInterface, UnitType} from "@microsoft/msfs-sdk"; 3 | import {intermediatePoint} from "./KLNNavmath"; 4 | 5 | /** 6 | * 3-33 7 | * Since the sim does not expose MSAs, we have to rely on our own data. The data was generated using a DEM and 8 | * does not account for obstacles. 9 | */ 10 | export class MSA { 11 | 12 | private msaTable: number[][] | undefined; 13 | private GEOPOINTCACHE: GeoPoint = new GeoPoint(0, 0); 14 | 15 | public getMSA(coord: LatLonInterface): Feet | null { 16 | if (coord.lat >= 75 || coord.lat < -56) { 17 | return null; 18 | } 19 | const latTable = this.msaTable![Math.floor(coord.lat) + 56]; 20 | return latTable[Math.floor(coord.lon) + 180]; 21 | } 22 | 23 | public getMSAFromTo(coord0: LatLonInterface, coord1: LatLonInterface): Feet | null { 24 | if (GeoPoint.equals(coord0, coord1)) { 25 | return this.getMSA(coord0); 26 | } 27 | 28 | let msa: number | null = 0; 29 | let f = 0; 30 | this.GEOPOINTCACHE.set(coord0); 31 | const fDiff = 40 / UnitType.GA_RADIAN.convertTo(this.GEOPOINTCACHE.distance(coord1), UnitType.NMILE); 32 | while (f <= 1) { 33 | const coord = intermediatePoint(coord0, coord1, f); 34 | const msa2 = this.getMSA(coord); 35 | if (msa2 === null || msa === null) { 36 | msa = null; 37 | } else { 38 | msa = Math.max(msa, msa2); 39 | } 40 | f = f + fDiff; 41 | } 42 | return msa; 43 | } 44 | 45 | public getMSAForRoute(coords: LatLonInterface[]): Feet | null { 46 | let esa: number | null = 0; 47 | for (let i = 1; i < coords.length; i++) { 48 | const legEsa = this.getMSAFromTo(coords[i - 1], coords[i]); 49 | if (legEsa === null) { 50 | return null 51 | } 52 | esa = Math.max(esa, legEsa); 53 | } 54 | return esa; 55 | } 56 | 57 | public async init(basePath: string): Promise { 58 | return new Promise((resolve, reject) => { 59 | const request = new XMLHttpRequest(); 60 | request.onreadystatechange = () => { 61 | if (request.readyState === XMLHttpRequest.DONE) { 62 | if (request.status === 200) { 63 | this.msaTable = JSON.parse(request.responseText); 64 | resolve(); 65 | } else { 66 | reject(`Could not initialize msa table: ${request.status}`); 67 | } 68 | } 69 | }; 70 | request.open('GET', `coui://${basePath}/Assets/msa.json`); 71 | request.send(); 72 | }); 73 | } 74 | } -------------------------------------------------------------------------------- /kln90b/controls/Button.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from '@microsoft/msfs-sdk'; 2 | import {EnterResult, Field} from "../pages/CursorController"; 3 | import {NO_CHILDREN} from '../pages/Page'; 4 | import {TickController} from "../TickController"; 5 | 6 | 7 | export class Button implements Field { 8 | readonly children = NO_CHILDREN; 9 | public isEntered = false; 10 | public isFocused = false; 11 | 12 | public isReadonly = false; 13 | public isVisible = true; 14 | protected readonly ref: NodeReference = FSComponent.createRef(); 15 | 16 | public constructor(public text: string, private readonly enterCallback: () => void, private readonly clearCallback?: () => void) { 17 | } 18 | 19 | 20 | public render(): VNode { 21 | return ( 22 | {this.text}); 23 | } 24 | 25 | 26 | public setFocused(focused: boolean) { 27 | this.isFocused = focused; 28 | } 29 | 30 | public setVisible(visible: boolean) { 31 | this.isVisible = visible; 32 | this.isReadonly = !visible; 33 | } 34 | 35 | outerLeft(): boolean { 36 | return false; 37 | } 38 | 39 | outerRight(): boolean { 40 | return false; 41 | } 42 | 43 | innerLeft(): boolean { 44 | return false; 45 | } 46 | 47 | innerRight(): boolean { 48 | return false; 49 | } 50 | 51 | isEnterAccepted(): boolean { 52 | return true; 53 | } 54 | 55 | enter(): Promise { 56 | this.enterCallback(); 57 | return Promise.resolve(EnterResult.Handled_Move_Focus); 58 | } 59 | 60 | tick(blink: boolean): void { 61 | if (!TickController.checkRef(this.ref)) { 62 | return; 63 | } 64 | if (this.isVisible) { 65 | this.ref!.instance.classList.remove("d-none"); 66 | } else { 67 | this.ref!.instance.classList.add("d-none"); 68 | } 69 | 70 | if (this.isFocused) { 71 | this.ref!.instance.classList.add("inverted", "inverted-blink"); 72 | if (blink) { 73 | this.ref!.instance.classList.add("inverted-blink"); 74 | } else { 75 | this.ref!.instance.classList.remove("inverted-blink"); 76 | } 77 | } else { 78 | this.ref!.instance.classList.remove("inverted", "inverted-blink"); 79 | } 80 | } 81 | 82 | clear(): boolean { 83 | if (this.clearCallback) { 84 | this.clearCallback(); 85 | return true; 86 | } 87 | return false; 88 | } 89 | 90 | isClearAccepted(): boolean { 91 | return this.clearCallback !== undefined; 92 | } 93 | 94 | public keyboard(key: string): boolean { 95 | return false; 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /kln90b/settings/RemarksManager.ts: -------------------------------------------------------------------------------- 1 | import {DefaultUserSettingManager, EventBus} from '@microsoft/msfs-sdk'; 2 | import {KLN90BUserSettings} from "./KLN90BUserSettings"; 3 | import { KLN90BUserFlightplansTypes} from "./KLN90BUserFlightplans"; 4 | import {KLN90BUserRemarkSettings, NUM_REMARKS} from "./KLN90BUserRemarkSettings"; 5 | 6 | export interface RemarksChangedEvent { 7 | changed: string; 8 | } 9 | 10 | /** 11 | * This class holds the remarks entered for airports on the apt 5 page (3-47) 12 | * We only use the ident and not the icao, since the ident is unique for airports and it's shorter for the actual settings 13 | */ 14 | export class RemarksManager { 15 | private remarks: { 16 | [ident: string]: [string, string, string]; 17 | } = {}; 18 | 19 | private manager: DefaultUserSettingManager; 20 | 21 | constructor(private readonly bus: EventBus, readonly userSettings: KLN90BUserSettings) { 22 | this.manager = KLN90BUserRemarkSettings.getManager(bus); 23 | this.loadRemarks(); 24 | console.log("Loaded remarks", this.remarks); 25 | } 26 | 27 | public saveRemarks(ident: string, remarks: [string, string, string]) { 28 | if (Object(this.remarks).length >= 100) { 29 | throw new Error("RMKS FULL"); 30 | } 31 | if (remarks[0] || remarks[1] || remarks[2]) { 32 | this.remarks[ident] = remarks; 33 | } else { 34 | delete this.remarks[ident]; 35 | } 36 | console.log("Remarks", this.remarks); 37 | 38 | 39 | this.saveAllRemarks(); 40 | 41 | this.bus.getPublisher().pub("changed", ident); 42 | } 43 | 44 | public deleteRemarks(ident: string): void { 45 | this.saveRemarks(ident, ["", "", ""]); 46 | } 47 | 48 | public getRemarks(ident: string): [string, string, string] { 49 | if (this.remarks.hasOwnProperty(ident)) { 50 | return this.remarks[ident]; 51 | } 52 | return [" ", " ", " "]; 53 | } 54 | 55 | public getAirportsWithRemarks(): string[] { 56 | return Object.keys(this.remarks).sort((a, b) => a.localeCompare(b)); 57 | } 58 | 59 | private loadRemarks() { 60 | for (let i = 0; i < NUM_REMARKS; i++) { 61 | const rmk = this.manager.getSetting(`rmk${i}`).get(); 62 | if (rmk !== "") { 63 | const ident = rmk.substring(0, 4); 64 | const lines = [...rmk.substring(4).match(/.{1,11}/g)!]; 65 | this.remarks[ident] = lines as [string, string, string]; 66 | } 67 | } 68 | } 69 | 70 | private saveAllRemarks() { 71 | let i = 0; 72 | for (const ident in this.remarks) { 73 | const joinedText = this.remarks[ident].join(""); 74 | this.manager.getSetting(`rmk${i}`).set(ident + joinedText); 75 | i++; 76 | } 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /kln90b/data/MessageHandler.ts: -------------------------------------------------------------------------------- 1 | import {CalcTickable} from "../TickController"; 2 | import {Sensors} from "../Sensors"; 3 | import {BoundaryUtils} from "./navdata/BoundaryUtils"; 4 | import {LodBoundary} from "@microsoft/msfs-sdk"; 5 | 6 | export interface Message { 7 | seen: boolean; 8 | readonly message: string[]; 9 | isConditionValid: () => boolean; 10 | } 11 | 12 | /** 13 | * This message will be shown once to the pilot and removes once seen 14 | */ 15 | export class OneTimeMessage implements Message { 16 | 17 | public seen: boolean = false; 18 | 19 | constructor(public readonly message: string[]) { 20 | } 21 | 22 | public isConditionValid(): boolean { 23 | return !this.seen; 24 | } 25 | } 26 | 27 | export class MessageHandler implements CalcTickable { 28 | 29 | //A lot of messages depent on state. All those messages are kept here 30 | public persistentMessages: Message[] = []; 31 | //The messages, that are currently presented to the user 32 | private activeMessages: Message[] = []; //todo clear messages when power is off 33 | 34 | public hasMessages(): boolean { 35 | return this.activeMessages.length > 0; 36 | } 37 | 38 | public getMessages(): Message[] { 39 | return this.activeMessages; 40 | } 41 | 42 | public hasUnreadMessages(): boolean { 43 | return this.activeMessages.filter(m => !m.seen).length > 0; 44 | } 45 | 46 | public addMessage(message: Message): void { 47 | this.activeMessages.push(message); 48 | } 49 | 50 | public tick() { 51 | this.activeMessages = this.activeMessages.filter(m => m.isConditionValid()); 52 | for (const message of this.persistentMessages) { 53 | const isValid = message.isConditionValid(); 54 | if (isValid) { 55 | if (!this.activeMessages.includes(message)) { 56 | this.activeMessages.push(message); 57 | } 58 | } else { 59 | message.seen = false; 60 | } 61 | } 62 | } 63 | } 64 | 65 | 66 | export class AirspaceAlertMessage extends OneTimeMessage { 67 | 68 | 69 | constructor(message: string[], private readonly airspace: LodBoundary, private readonly sensors: Sensors) { 70 | super(message); 71 | } 72 | 73 | public isConditionValid(): boolean { 74 | //TODO need to check that we are still closing in 75 | return super.isConditionValid() && !BoundaryUtils.isInside(this.airspace, this.sensors.in.gps.coords.lat, this.sensors.in.gps.coords.lon); 76 | } 77 | } 78 | 79 | export class InsideAirspaceMessage extends OneTimeMessage { 80 | 81 | 82 | constructor(message: string[], private readonly airspace: LodBoundary, private readonly sensors: Sensors) { 83 | super(message); 84 | } 85 | 86 | public isConditionValid(): boolean { 87 | return super.isConditionValid() && BoundaryUtils.isInside(this.airspace, this.sensors.in.gps.coords.lat, this.sensors.in.gps.coords.lon); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /kln90b/controls/editors/DateEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Editor, Rawvalue} from "./Editor"; 2 | import {EventBus, FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 3 | import {MonthEditorField, NumberEditorField} from "./EditorField"; 4 | import {shortYearToLongYear, TimeStamp} from "../../data/Time"; 5 | import {TickController} from "../../TickController"; 6 | 7 | export class DateEditor extends Editor { 8 | 9 | private space1Ref: NodeReference = FSComponent.createRef(); 10 | private space2Ref: NodeReference = FSComponent.createRef(); 11 | 12 | constructor(bus: EventBus, value: TimeStamp, enterCallback: (text: TimeStamp) => void) { 13 | super(bus, [ 14 | NumberEditorField.createWithMinMax(1, 31), 15 | new MonthEditorField(), 16 | new NumberEditorField(), 17 | new NumberEditorField(), 18 | ], value, enterCallback); 19 | 20 | //The default date in the -89 is 2000, but it's 1988 in the -90B 21 | //When entering a 0 in the third place and leaving the fourth place blank, it becomes 2008 22 | this.editorFields[2].defaultValue = 8; 23 | this.editorFields[3].defaultValue = 8; 24 | } 25 | 26 | public render(): VNode { 27 | return ( 28 | {this.editorFields[0].render()} {this.editorFields[1].render()} {this.editorFields[2].render()}{this.editorFields[3].render()} 30 | ); 31 | } 32 | 33 | protected convertFromValue(value: TimeStamp): Rawvalue { 34 | return [ 35 | value.getDate() - 1, 36 | value.getMonth(), 37 | Number(String(value.getYear()).substring(2, 3)), 38 | Number(String(value.getYear()).substring(3, 4)), 39 | ]; 40 | } 41 | 42 | protected convertToValue(rawValue: Rawvalue): Promise { 43 | const year = shortYearToLongYear(Number(String(rawValue[2]) + String(rawValue[3]))); 44 | const month = rawValue[1]; 45 | const date = rawValue[0] + 1; 46 | 47 | const newDate = this.value!.withDate(year, month, date); 48 | if (newDate.getYear() != year || newDate.getMonth() != month || newDate.getDate() != date) { //The entered date was invalid, like Nov 31. 49 | return Promise.resolve(null); 50 | } 51 | return Promise.resolve(newDate); 52 | } 53 | 54 | tick(blink: boolean): void { 55 | super.tick(blink); 56 | if (!TickController.checkRef(this.space1Ref, this.space2Ref)) { 57 | return; 58 | } 59 | if (this.isFocused) { 60 | this.space1Ref!.instance.classList.add("inverted"); 61 | this.space2Ref!.instance.classList.add("inverted"); 62 | } else { 63 | this.space1Ref!.instance.classList.remove("inverted"); 64 | this.space2Ref!.instance.classList.remove("inverted"); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/SuperDeviationBar.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from "@microsoft/msfs-sdk"; 2 | import {DeviationBar} from "./DeviationBar"; 3 | 4 | /* 5 | If this looks Cyrillic to you, then because it is. 6 | The Greek characters are mapped to the deviation scale from full left to full right. 7 | */ 8 | const DOT_LETTERS = ["Ѐ", "Ё", "Ђ", "Ѓ", "Є", "Ѕ", "І", "Ї", "Ј", "Љ"]; 9 | const TO_LETTERS = ["Њ", "Ћ", "Ќ", "Ѝ", "Ў", "Џ", "А", "Б", "В", "Г"]; 10 | const FROM_LETTERS = ["Д", "Е", "Ж", "З", "И", "Й", "К", "Л", "М", "Н"]; 11 | const SPACE_LETTERS = ["О", "П", "Р", "С", "Т", "У", "Ф", "Х", "Ц", "Ч"]; 12 | 13 | /** 14 | * https://youtu.be/gjmVrkHTdP0?t=103 Example for flagged Super NAV 1 15 | */ 16 | export class SuperDeviationBar extends DeviationBar { 17 | 18 | 19 | render(): VNode { 20 | return ( 21 | 22 |  Ш Ш Ш ШF L A GШ Ш Ш Ш 24 | ); 25 | } 26 | 27 | protected buildDeviationScale(): string { 28 | const scale = this.to ? " Ш Ш Ш Ш Ш Щ Ш Ш Ш Ш Ш " : " Ш Ш Ш Ш Ш Ъ Ш Ш Ш Ш Ш "; 29 | const dev = -Utils.Clamp(this.deviation! / this.xtkScale, -1, 1); 30 | 31 | //0-22 -1:0 0:5 0.5: 7 1: 22 32 | let charToReplace = (dev + 1) * 11; 33 | //0-9 0:4 exakt, 0.1:5, 0.2:6, 0.3:7, 0.4:8, 0.5:0, 0.6:1, 0.7:1, 0.8:2, 0.9:3 34 | const subdeviation = (charToReplace % 1); 35 | const targetLetterIndex = Math.floor(((subdeviation + 0.5) * 9) % 9); 36 | charToReplace = Math.round(charToReplace); 37 | 38 | 39 | let targetChar; 40 | if (charToReplace == 11) { 41 | targetChar = this.to ? TO_LETTERS[targetLetterIndex] : FROM_LETTERS[targetLetterIndex]; 42 | } else if (charToReplace % 2 == 0) { 43 | targetChar = SPACE_LETTERS[targetLetterIndex]; 44 | } else { 45 | targetChar = DOT_LETTERS[targetLetterIndex]; 46 | } 47 | 48 | //Between two chars 49 | if (targetLetterIndex == 9 && charToReplace < 22) { 50 | if (charToReplace == 10) { 51 | targetChar += this.to ? TO_LETTERS[0] : FROM_LETTERS[0]; 52 | } else if (charToReplace % 2 == 1) { 53 | targetChar += SPACE_LETTERS[0]; 54 | } else { 55 | targetChar += DOT_LETTERS[0]; 56 | } 57 | } else if (targetLetterIndex == 0 && charToReplace > 0) { 58 | if (charToReplace == 12) { 59 | targetChar = this.to ? TO_LETTERS[9] : FROM_LETTERS[9] + targetChar; 60 | } else if (charToReplace % 2 == 1) { 61 | targetChar = SPACE_LETTERS[9] + targetChar; 62 | } else { 63 | targetChar = DOT_LETTERS[9] + targetChar; 64 | } 65 | charToReplace--; 66 | } 67 | 68 | return this.replaceStringAt(scale, charToReplace, targetChar); 69 | 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/FpmFieldset.tsx: -------------------------------------------------------------------------------- 1 | import {SelectField} from "./SelectField"; 2 | import {FSComponent, VNode} from "@microsoft/msfs-sdk"; 3 | import {UiElement, UIElementChildren} from "../../pages/Page"; 4 | import {format} from "numerable"; 5 | import {Knots} from "../../data/Units"; 6 | 7 | 8 | type FpmFieldsetTypes = { 9 | Fpm1000: SelectField; 10 | Fpm100: SelectField; 11 | Fpm10: SelectField; 12 | Fpm1: SelectField; 13 | } 14 | 15 | const FPM_SET = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; 16 | 17 | export class FpmFieldset implements UiElement { 18 | 19 | 20 | readonly children: UIElementChildren; 21 | 22 | constructor(private fpm: number, private readonly callback: (fpm: number) => void) { 23 | const FpmString = format(fpm, "0000"); 24 | this.children = new UIElementChildren({ 25 | Fpm1000: new SelectField(FPM_SET, Number(FpmString.substring(0, 1)), this.saveFpm1000.bind(this)), 26 | Fpm100: new SelectField(FPM_SET, Number(FpmString.substring(1, 2)), this.saveFpm100.bind(this)), 27 | Fpm10: new SelectField(FPM_SET, Number(FpmString.substring(2, 3)), this.saveFpm10.bind(this)), 28 | Fpm1: new SelectField(FPM_SET, Number(FpmString.substring(3, 4)), this.saveFpm1.bind(this)), 29 | }); 30 | 31 | this.children.get("Fpm10").isReadonly = true; 32 | this.children.get("Fpm1").isReadonly = true; 33 | } 34 | 35 | render(): VNode { 36 | return ( 37 | {this.children.get("Fpm1000").render()}{this.children.get("Fpm100").render()}{this.children.get("Fpm10").render()}{this.children.get("Fpm1").render()}); 38 | } 39 | 40 | tick(blink: boolean): void { 41 | } 42 | 43 | public setFpm(Fpm: Knots): void { 44 | const FpmString = format(Fpm, "0000"); 45 | this.children.get("Fpm1000").value = Number(FpmString.substring(0, 1)); 46 | this.children.get("Fpm100").value = Number(FpmString.substring(1, 2)); 47 | this.children.get("Fpm10").value = Number(FpmString.substring(2, 3)); 48 | this.children.get("Fpm1").value = Number(FpmString.substring(3, 4)); 49 | 50 | } 51 | 52 | private saveFpm1000(newFpm1000: number): void { 53 | const oldFpm = format(this.fpm, "0000"); 54 | this.fpm = Number(newFpm1000 + oldFpm.substring(1)); 55 | this.callback(this.fpm); 56 | } 57 | 58 | private saveFpm100(newFpm100: number): void { 59 | const oldFpm = format(this.fpm, "0000"); 60 | this.fpm = Number(oldFpm.substring(0, 1) + newFpm100 + oldFpm.substring(2)); 61 | this.callback(this.fpm); 62 | } 63 | 64 | private saveFpm10(newFpm10: number): void { 65 | const oldFpm = format(this.fpm, "0000"); 66 | this.fpm = Number(oldFpm.substring(0, 2) + newFpm10 + oldFpm.substring(3)); 67 | this.callback(this.fpm); 68 | } 69 | 70 | private saveFpm1(newFpm1: number): void { 71 | const oldFpm = format(this.fpm, "0000"); 72 | this.fpm = Number(oldFpm.substring(0, 3) + newFpm1); 73 | this.callback(this.fpm); 74 | } 75 | 76 | 77 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/FuelDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 2 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 3 | import {TickController} from "../../TickController"; 4 | import {format} from "numerable"; 5 | 6 | /** 7 | * Displays fuel on the TRIP page 8 | */ 9 | export class TripFuelDisplay implements UiElement { 10 | readonly children = NO_CHILDREN; 11 | private readonly ref: NodeReference = FSComponent.createRef(); 12 | 13 | constructor(public fuel: number | null = null) { 14 | } 15 | 16 | render(): VNode { 17 | return ({this.formatFuel()}); 18 | } 19 | 20 | public tick(blink: boolean): void { 21 | if (!TickController.checkRef(this.ref)) { 22 | return; 23 | } 24 | 25 | this.ref.instance.innerText = this.formatFuel(); 26 | } 27 | 28 | private formatFuel(): string { 29 | if (this.fuel === null) { 30 | return "---.-"; 31 | } 32 | 33 | const actFuel = Math.min(this.fuel, 99999); 34 | 35 | if (actFuel >= 100) { //Number is too large for decimals 36 | return format(actFuel, "00").padStart(5, " "); 37 | } else { 38 | return format(actFuel, "0.0").padStart(5, " "); 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Displays fuel on the OTH page 45 | */ 46 | export class OthFuelDisplay implements UiElement { 47 | readonly children = NO_CHILDREN; 48 | private readonly ref: NodeReference = FSComponent.createRef(); 49 | 50 | constructor(public fuel: number | null = null) { 51 | } 52 | 53 | render(): VNode { 54 | return ({this.formatFuel()}); 55 | } 56 | 57 | public tick(blink: boolean): void { 58 | if (!TickController.checkRef(this.ref)) { 59 | return; 60 | } 61 | this.ref.instance.innerText = this.formatFuel(); 62 | } 63 | 64 | private formatFuel(): string { 65 | if (this.fuel === null) { 66 | return "-----"; 67 | } 68 | 69 | const actFuel = Math.max(Math.min(this.fuel, 99999), 0); 70 | 71 | return format(actFuel, "0").padStart(5, " "); 72 | 73 | } 74 | } 75 | 76 | 77 | /** 78 | * Displays fuel flow on the OTH 7 page. For some reason, this is left aligned? 79 | */ 80 | export class FuelFlowDisplay implements UiElement { 81 | readonly children = NO_CHILDREN; 82 | private readonly ref: NodeReference = FSComponent.createRef(); 83 | 84 | constructor(public fuelFlow: number) { 85 | } 86 | 87 | render(): VNode { 88 | return ({this.formatFuelFlow()}); 89 | } 90 | 91 | public tick(blink: boolean): void { 92 | if (!TickController.checkRef(this.ref)) { 93 | return; 94 | } 95 | this.ref.instance.innerText = this.formatFuelFlow(); 96 | } 97 | 98 | private formatFuelFlow(): string { 99 | const actFuel = Math.min(this.fuelFlow, 9999); 100 | 101 | return format(actFuel, "0").padEnd(4, " "); 102 | 103 | } 104 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/MapOrientationSelector.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, UserSetting} from "@microsoft/msfs-sdk"; 2 | import {SelectField} from "./SelectField"; 3 | import {KLN90PlaneSettings} from "../../settings/KLN90BPlaneSettings"; 4 | import {TickController} from "../../TickController"; 5 | import {format} from "numerable"; 6 | import {Sensors} from "../../Sensors"; 7 | import {Nav5Orientation} from "../../settings/KLN90BUserSettings"; 8 | import {ModeController} from "../../services/ModeController"; 9 | import {KLNMagvar} from "../../data/navdata/KLNMagvar"; 10 | 11 | 12 | export class MapOrientationSelector extends SelectField { 13 | 14 | 15 | private constructor(valueSet: string[], private readonly orientationSetting: UserSetting, private readonly sensors: Sensors, private readonly modeController: ModeController, private readonly magvar: KLNMagvar, changedCallback: (value: number) => void) { 16 | super(valueSet, orientationSetting.get(), changedCallback); 17 | } 18 | 19 | public static build(options: KLN90PlaneSettings, orientationSetting: UserSetting, sensors: Sensors, modeController: ModeController, magvar: KLNMagvar): MapOrientationSelector { 20 | return new MapOrientationSelector(this.getValueset(options), orientationSetting, sensors, modeController, magvar, (orientation) => this.saveMapOrientation(orientationSetting, orientation)); 21 | } 22 | 23 | private static getValueset(options: KLN90PlaneSettings): string[] { 24 | return options.input.headingInput ? ["N^ ", "DTK^", "TK^ ", "HDG^"] : ["N^ ", "DTK^", "TK^ "]; 25 | } 26 | 27 | private static saveMapOrientation(orientationSetting: UserSetting, orientation: Nav5Orientation): void { 28 | orientationSetting.set(orientation); 29 | } 30 | 31 | /** 32 | * 3-35 33 | * @param blink 34 | */ 35 | tick(blink: boolean): void { 36 | if (!TickController.checkRef(this.ref)) { 37 | return; 38 | } 39 | 40 | if (this.isFocused) { 41 | this.ref.instance.textContent = this.valueSet[this.value]; 42 | this.ref!.instance.classList.add("inverted"); 43 | } else { 44 | this.ref!.instance.classList.remove("inverted"); 45 | 46 | switch (this.value) { 47 | case Nav5Orientation.NORTH_UP: 48 | this.ref.instance.textContent = "N^ "; 49 | break; 50 | case Nav5Orientation.DTK_UP: 51 | this.ref.instance.textContent = this.formatDegrees(this.modeController.getDtkOrObsMagnetic()); 52 | break; 53 | case Nav5Orientation.TK_UP: 54 | this.ref.instance.textContent = this.formatDegrees(this.magvar.trueToMag(this.sensors.in.gps.getTrackTrueRespectingGroundspeed())); 55 | break; 56 | case Nav5Orientation.HDG_UP: 57 | this.ref.instance.textContent = this.formatDegrees(this.sensors.in.headingGyro); 58 | break; 59 | } 60 | } 61 | } 62 | 63 | private formatDegrees(degrees: number | null): string { 64 | if (degrees === null) { 65 | return "---°"; 66 | } 67 | return `${format(degrees, "000")}°`; 68 | } 69 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Cal4Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, UnitType, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {Knots} from "../../data/Units"; 6 | import {SpeedFieldset} from "../../controls/selects/SpeedFieldset"; 7 | import {VnavAngleFieldset} from "../../controls/selects/VnavFieldsets"; 8 | import {FpmFieldset} from "../../controls/selects/FpmFieldset"; 9 | 10 | 11 | type Cal4PageTypes = { 12 | gs: SpeedFieldset, 13 | fpm: FpmFieldset, 14 | angle: VnavAngleFieldset, 15 | } 16 | 17 | /** 18 | * 5-12 19 | */ 20 | export class Cal4Page extends SixLineHalfPage { 21 | 22 | public readonly cursorController; 23 | readonly children: UIElementChildren; 24 | 25 | readonly name: string = "CAL 4"; 26 | 27 | constructor(props: PageProps) { 28 | super(props); 29 | 30 | this.children = new UIElementChildren({ 31 | gs: new SpeedFieldset(this.props.userSettings.getSetting('cal4GS').get(), this.setGS.bind(this)), 32 | fpm: new FpmFieldset(this.props.userSettings.getSetting('cal4Fpm').get(), this.setFpm.bind(this)), 33 | angle: new VnavAngleFieldset(this.props.userSettings.getSetting('cal4Angle').get(), this.setAngle.bind(this)), 34 | }); 35 | 36 | this.cursorController = new CursorController(this.children); 37 | } 38 | 39 | 40 | public render(): VNode { 41 | return (
42 |              VNV ANGLE
43 |
44 | GS:   {this.children.get("gs").render()}kt
45 | FPM:   {this.children.get("fpm").render()}
46 | ANGLE:{this.children.get("angle").render()}° 47 |
); 48 | 49 | 50 | } 51 | 52 | private setGS(gs: Knots): void { 53 | this.props.userSettings.getSetting('cal4GS').set(gs); 54 | let angle = 0; 55 | if (gs > 0) { 56 | angle = Math.atan(this.props.userSettings.getSetting('cal4Fpm').get() / UnitType.KNOT.convertTo(gs, UnitType.FPM)) * Avionics.Utils.RAD2DEG; 57 | } 58 | this.props.userSettings.getSetting('cal4Angle').set(angle); 59 | this.children.get("angle").setValue(angle); 60 | } 61 | 62 | private setFpm(fpm: number): void { 63 | this.props.userSettings.getSetting('cal4Fpm').set(fpm); 64 | let angle = 0; 65 | if (this.props.userSettings.getSetting('cal4GS').get() > 0) { 66 | angle = Math.atan(fpm / UnitType.KNOT.convertTo(this.props.userSettings.getSetting('cal4GS').get(), UnitType.FPM)) * Avionics.Utils.RAD2DEG; 67 | } 68 | this.props.userSettings.getSetting('cal4Angle').set(angle); 69 | this.children.get("angle").setValue(angle); 70 | } 71 | 72 | private setAngle(angle: number): void { 73 | this.props.userSettings.getSetting('cal4Angle').set(angle); 74 | const fpm = Math.round(UnitType.KNOT.convertTo(this.props.userSettings.getSetting('cal4GS').get(), UnitType.FPM) * Math.tan(angle * Avionics.Utils.DEG2RAD) / 100) * 100; 75 | 76 | this.props.userSettings.getSetting('cal4Fpm').set(fpm); 77 | this.children.get("fpm").setFpm(fpm); 78 | 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /kln90b/controls/selects/NearestSelector.tsx: -------------------------------------------------------------------------------- 1 | import {Facility, FSComponent, NodeReference, VNode} from '@microsoft/msfs-sdk'; 2 | import {EnterResult, Field} from "../../pages/CursorController"; 3 | import {NO_CHILDREN} from '../../pages/Page'; 4 | import {TickController} from "../../TickController"; 5 | import {NearestWpt} from "../../data/navdata/NearestList"; 6 | import {isNearestWpt} from "../../pages/right/WaypointPage"; 7 | 8 | 9 | export class NearestSelector implements Field { 10 | 11 | readonly children = NO_CHILDREN; 12 | public isEntered = false; 13 | public isFocused = false; 14 | public isReadonly = false; 15 | protected readonly ref: NodeReference = FSComponent.createRef(); 16 | 17 | /** 18 | * The nearestIndex is 0 based 19 | * @param facility 20 | */ 21 | public constructor(private facility: Facility | NearestWpt | null) { 22 | } 23 | 24 | public render(): VNode { 25 | return ( 26 | {this.formatValue(this.getIndex())}); 27 | } 28 | 29 | outerLeft(): boolean { 30 | return false; 31 | } 32 | 33 | outerRight(): boolean { 34 | return false; 35 | } 36 | 37 | innerLeft(): boolean { 38 | return false; 39 | } 40 | 41 | innerRight(): boolean { 42 | return false; 43 | } 44 | 45 | public setFacility(facility: Facility | NearestWpt | null): void { 46 | this.facility = facility; 47 | } 48 | 49 | public setFocused(focused: boolean) { 50 | this.isFocused = focused; 51 | } 52 | 53 | tick(blink: boolean): void { 54 | if (!TickController.checkRef(this.ref)) { 55 | return; 56 | } 57 | const nearestIndex = this.getIndex(); 58 | this.isReadonly = nearestIndex === -1; 59 | 60 | this.ref.instance.textContent = this.formatValue(nearestIndex); 61 | 62 | if (this.isFocused) { 63 | this.ref!.instance.classList.add("inverted"); 64 | this.ref!.instance.classList.remove("blink"); 65 | } else { 66 | this.ref!.instance.classList.remove("inverted"); 67 | if (blink && nearestIndex !== -1) { 68 | this.ref!.instance.classList.add("blink"); 69 | } else { 70 | this.ref!.instance.classList.remove("blink"); 71 | } 72 | } 73 | } 74 | 75 | private getIndex(): number { 76 | return isNearestWpt(this.facility) ? this.facility.index : -1; 77 | } 78 | 79 | isEnterAccepted(): boolean { 80 | return false; 81 | } 82 | 83 | enter(): Promise { 84 | return Promise.resolve(EnterResult.Not_Handled); 85 | } 86 | 87 | clear(): boolean { 88 | return false; 89 | } 90 | 91 | isClearAccepted(): boolean { 92 | return false; 93 | } 94 | 95 | private formatValue(nearestIndex: number) { 96 | if (nearestIndex === -1) { 97 | return " "; 98 | } 99 | 100 | return `nr ${nearestIndex + 1}`; //The KLN is 1 based 101 | } 102 | 103 | public keyboard(key: string): boolean { 104 | return false; 105 | } 106 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Cal1Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {AltitudeFieldset} from "../../controls/selects/AltitudeFieldset"; 6 | import {BaroFieldset, BaroFieldsetFactory} from "../../controls/selects/BaroFieldset"; 7 | import {TempFieldset} from "../../controls/selects/TempFieldset"; 8 | import {AltitudeDisplay} from "../../controls/displays/AltitudeDisplay"; 9 | import {Celsius, Feet, Inhg} from "../../data/Units"; 10 | import {indicatedAlt2PressureAlt, pressureAlt2DensityAlt} from "../../data/Conversions"; 11 | 12 | 13 | type Cal1PageTypes = { 14 | indicated: AltitudeFieldset, 15 | baro: BaroFieldset, 16 | pressure: AltitudeDisplay, 17 | sat: TempFieldset, 18 | density: AltitudeDisplay, 19 | } 20 | 21 | /** 22 | * 5-10 23 | */ 24 | export class Cal1Page extends SixLineHalfPage { 25 | 26 | public readonly cursorController; 27 | readonly children: UIElementChildren; 28 | 29 | readonly name: string = "CAL 1"; 30 | 31 | constructor(props: PageProps) { 32 | super(props); 33 | 34 | this.children = new UIElementChildren({ 35 | indicated: new AltitudeFieldset(this.props.userSettings.getSetting('cal12IndicatedAltitude').get(), this.setIndicatedAltitude.bind(this)), 36 | baro: BaroFieldsetFactory.createBaroFieldSet(this.props.userSettings.getSetting('cal12Barometer').get(), this.props.userSettings, this.setBarometer.bind(this)), 37 | pressure: new AltitudeDisplay(null), 38 | sat: new TempFieldset(this.props.userSettings.getSetting('cal1SAT').get(), this.setTemp.bind(this)), 39 | density: new AltitudeDisplay(null), 40 | }); 41 | this.cursorController = new CursorController(this.children); 42 | } 43 | 44 | 45 | public render(): VNode { 46 | return (
47 |               ALTITUDE
48 | IND:{this.children.get("indicated").render()}ft
49 | BARO:{this.children.get("baro").render()}
50 | PRS {this.children.get("pressure").render()}ft
51 | TEMP: {this.children.get("sat").render()}°C
52 | DEN {this.children.get("density").render()}ft 53 |
); 54 | } 55 | 56 | protected redraw() { 57 | const pressureAlt = indicatedAlt2PressureAlt(this.props.userSettings.getSetting('cal12IndicatedAltitude').get(), this.props.userSettings.getSetting('cal12Barometer').get()); 58 | this.children.get("pressure").altitude = pressureAlt; 59 | this.children.get("density").altitude = pressureAlt2DensityAlt(pressureAlt, this.props.userSettings.getSetting('cal1SAT').get()); 60 | 61 | } 62 | 63 | private setIndicatedAltitude(alt: Feet): void { 64 | this.props.userSettings.getSetting('cal12IndicatedAltitude').set(alt); 65 | this.requiresRedraw = true; 66 | } 67 | 68 | private setBarometer(baro: Inhg): void { 69 | this.props.userSettings.getSetting('cal12Barometer').set(baro); 70 | this.requiresRedraw = true; 71 | } 72 | 73 | private setTemp(temp: Celsius): void { 74 | this.props.userSettings.getSetting('cal1SAT').set(temp); 75 | this.requiresRedraw = true; 76 | } 77 | } -------------------------------------------------------------------------------- /kln90b/controls/displays/FlightplanArrow.tsx: -------------------------------------------------------------------------------- 1 | import {NO_CHILDREN, UiElement} from "../../pages/Page"; 2 | import {FSComponent, NodeReference, VNode} from "@microsoft/msfs-sdk"; 3 | import {TickController} from "../../TickController"; 4 | import {NavPageState} from "../../data/VolatileMemory"; 5 | import {CursorController} from "../../pages/CursorController"; 6 | 7 | /** 8 | * 4-7, 4-8 9 | */ 10 | export class FlightplanArrow implements UiElement { 11 | readonly children = NO_CHILDREN; 12 | public isVisible: boolean = true; 13 | private ref: NodeReference = FSComponent.createRef(); 14 | 15 | constructor(public readonly idx: number, private readonly navPageState: NavPageState, private readonly cursorController: CursorController) { 16 | } 17 | 18 | render(): VNode { 19 | return ( ); 20 | } 21 | 22 | public tick(blink: boolean): void { 23 | if (!TickController.checkRef(this.ref)) { 24 | return; 25 | } 26 | if (this.isVisible) { 27 | this.ref.instance.classList.remove("d-none"); 28 | } else { 29 | this.ref.instance.classList.add("d-none"); 30 | return; 31 | } 32 | 33 | //remove all arrows when editing: https://youtu.be/-7xleA3Hz3Y?t=435 34 | if (this.cursorController.getCurrentFocusedField()?.isEntered) { 35 | this.ref.instance.innerText = " "; 36 | this.ref.instance.classList.remove("blink"); 37 | return; 38 | } 39 | 40 | const activeIdx = this.navPageState.activeWaypoint.getActiveFplIdx(); 41 | if (activeIdx === null) { 42 | this.ref.instance.innerText = " "; 43 | this.ref.instance.classList.remove("blink"); 44 | } else { 45 | if (activeIdx === this.idx && this.idx !== -1) { //The active waypoint 46 | if (this.navPageState.activeWaypoint.isDctNavigation()) { 47 | this.ref.instance.innerText = "›"; 48 | } else { 49 | this.ref.instance.innerText = "À"; 50 | } 51 | 52 | //3-29 Waypoint alerting 53 | if (this.navPageState.waypointAlert && blink) { 54 | this.ref.instance.classList.add("blink"); 55 | } else { 56 | this.ref.instance.classList.remove("blink"); 57 | } 58 | } else if (activeIdx - 1 === this.idx && this.idx !== -1) { //The from waypoint 59 | if (this.navPageState.activeWaypoint.isDctNavigation()) { 60 | this.ref.instance.innerText = " "; 61 | } else { 62 | this.ref.instance.innerText = "Á"; 63 | } 64 | this.ref.instance.classList.remove("blink"); 65 | } else if (activeIdx === this.idx + 0.5 && this.idx !== -1) { //Procedure between two waypoints 66 | if (this.navPageState.activeWaypoint.isDctNavigation()) { 67 | this.ref.instance.innerText = " "; 68 | } else { 69 | this.ref.instance.innerText = "Â"; 70 | } 71 | this.ref.instance.classList.remove("blink"); 72 | } else { //Something else 73 | this.ref.instance.innerText = " "; 74 | this.ref.instance.classList.remove("blink"); 75 | } 76 | 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /kln90b/pages/left/Cal5Page.tsx: -------------------------------------------------------------------------------- 1 | import {FSComponent, UnitType, VNode} from '@microsoft/msfs-sdk'; 2 | import {SixLineHalfPage} from "../FiveSegmentPage"; 3 | import {PageProps, UIElementChildren} from "../Page"; 4 | import {CursorController} from "../CursorController"; 5 | import {Celsius, Knots, Mph} from "../../data/Units"; 6 | import {SpeedFieldset} from "../../controls/selects/SpeedFieldset"; 7 | import {TempFieldset} from "../../controls/selects/TempFieldset"; 8 | 9 | 10 | type Cal5PageTypes = { 11 | tempC: TempFieldset, 12 | tempF: TempFieldset, 13 | speedKt: SpeedFieldset, 14 | speedMph: SpeedFieldset, 15 | } 16 | 17 | /** 18 | * 5-13 19 | */ 20 | export class Cal5Page extends SixLineHalfPage { 21 | 22 | public readonly cursorController; 23 | readonly children: UIElementChildren; 24 | 25 | readonly name: string = "CAL 5"; 26 | 27 | constructor(props: PageProps) { 28 | super(props); 29 | 30 | this.children = new UIElementChildren({ 31 | tempC: new TempFieldset(this.props.userSettings.getSetting('cal5TempC').get(), this.setTempC.bind(this)), 32 | tempF: new TempFieldset(this.props.userSettings.getSetting('cal5TempF').get(), this.setTempF.bind(this)), 33 | speedKt: new SpeedFieldset(this.props.userSettings.getSetting('cal5SpeedKt').get(), this.setSpeedKt.bind(this)), 34 | speedMph: new SpeedFieldset(this.props.userSettings.getSetting('cal5SpeedMph').get(), this.setSpeedMph.bind(this)), 35 | }); 36 | 37 | this.cursorController = new CursorController(this.children); 38 | } 39 | 40 | 41 | public render(): VNode { 42 | return (
43 |             TEMP/SPEED
44 |    {this.children.get("tempC").render()}°C
45 |    {this.children.get("tempF").render()}°F
46 |
47 |    {this.children.get("speedKt").render()}kt
48 |    {this.children.get("speedMph").render()}mph 49 |
); 50 | 51 | 52 | } 53 | 54 | private setTempC(tempC: Celsius): void { 55 | this.props.userSettings.getSetting('cal5TempC').set(tempC); 56 | const tempF = UnitType.CELSIUS.convertTo(tempC, UnitType.FAHRENHEIT); 57 | this.props.userSettings.getSetting('cal5TempF').set(tempF); 58 | this.children.get("tempF").setTemp(tempF); 59 | } 60 | 61 | private setTempF(tempF: Celsius): void { 62 | this.props.userSettings.getSetting('cal5TempF').set(tempF); 63 | const tempC = UnitType.FAHRENHEIT.convertTo(tempF, UnitType.CELSIUS); 64 | this.props.userSettings.getSetting('cal5TempC').set(tempC); 65 | this.children.get("tempC").setTemp(tempC); 66 | } 67 | 68 | private setSpeedKt(speedKt: Knots): void { 69 | this.props.userSettings.getSetting('cal5SpeedKt').set(speedKt); 70 | const speedMph = UnitType.KNOT.convertTo(speedKt, UnitType.MPH); 71 | this.props.userSettings.getSetting('cal5SpeedMph').set(speedMph); 72 | this.children.get("speedMph").setSpeed(speedMph); 73 | } 74 | 75 | private setSpeedMph(speedMph: Mph): void { 76 | this.props.userSettings.getSetting('cal5SpeedMph').set(speedMph); 77 | const speedKt = UnitType.MPH.convertTo(speedMph, UnitType.KNOT); 78 | this.props.userSettings.getSetting('cal5SpeedKt').set(speedKt); 79 | this.children.get("speedKt").setSpeed(speedKt); 80 | } 81 | } --------------------------------------------------------------------------------